diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx index 7ae01e1..090efdd 100644 --- a/src/components/Bars.tsx +++ b/src/components/Bars.tsx @@ -1,13 +1,6 @@ import { Typewriter } from "./Typewriter"; import { useBars } from "~/context/bars"; -import { - onMount, - createSignal, - Show, - For, - onCleanup, - createEffect -} from "solid-js"; +import { onMount, createSignal, Show, For, onCleanup } from "solid-js"; import { api } from "~/lib/api"; import { insertSoftHyphens } from "~/lib/client-utils"; import GitHub from "./icons/GitHub"; @@ -102,7 +95,7 @@ export function RightBarContent() { const handleLinkClick = () => { if ( typeof window !== "undefined" && - window.innerWidth < BREAKPOINTS.MOBILE + window.innerWidth < BREAKPOINTS.MOBILE_MAX_WIDTH ) { setLeftBarVisible(false); } diff --git a/src/components/blog/AddAttachmentSection.tsx b/src/components/blog/AddAttachmentSection.tsx index 49e2d93..a1d8f26 100644 --- a/src/components/blog/AddAttachmentSection.tsx +++ b/src/components/blog/AddAttachmentSection.tsx @@ -1,6 +1,6 @@ import { createSignal, createEffect, For, Show } from "solid-js"; import Dropzone from "./Dropzone"; -import XCircle from "~/components/icons/XCircle"; +import AttachmentThumbnail from "~/components/ui/AttachmentThumbnail"; import AddImageToS3 from "~/lib/s3upload"; import { env } from "~/env/client"; import { api } from "~/lib/api"; @@ -147,42 +147,13 @@ export default function AddAttachmentSection(props: AddAttachmentSectionProps) {
{(file) => ( -
- - -
+ copyToClipboard(file.key)} + onRemove={() => removeImage(file.key)} + alt="attachment" + /> )}
0}> @@ -190,45 +161,17 @@ export default function AddAttachmentSection(props: AddAttachmentSectionProps) { {(file, index) => ( -
- - -
+ + copyToClipboard(newFileHolderKeys()[index()] as string) + } + onRemove={() => + removeNewImage(index(), newFileHolderKeys()[index()]) + } + alt="new attachment" + /> )}
diff --git a/src/components/blog/CommentBlock.tsx b/src/components/blog/CommentBlock.tsx index 9741e88..7e6a362 100644 --- a/src/components/blog/CommentBlock.tsx +++ b/src/components/blog/CommentBlock.tsx @@ -10,7 +10,8 @@ import ReplyIcon from "~/components/icons/ReplyIcon"; import TrashIcon from "~/components/icons/TrashIcon"; import EditIcon from "~/components/icons/EditIcon"; import ThumbsUpEmoji from "~/components/icons/emojis/ThumbsUp"; -import LoadingSpinner from "~/components/LoadingSpinner"; +import Button from "~/components/ui/Button"; +import IconButton from "~/components/ui/IconButton"; import CommentInputBlock from "./CommentInputBlock"; import ReactionBar from "./ReactionBar"; @@ -254,32 +255,46 @@ export default function CommentBlock(props: CommentBlockProps) { {/* Delete button */} - + } + variant="danger" + loading={deletionLoading()} + onClick={deleteCommentTrigger} + aria-label="Delete comment" + class="z-100" + /> {/* Edit and Reply buttons */}
- + } + onClick={editCommentTrigger} + aria-label="Edit comment" + class="px-2" + /> - + + } + onClick={toggleCommentReplyBox} + aria-label="Reply to comment" + class="z-30" + />
{/* Reaction bar */} diff --git a/src/components/blog/CommentInputBlock.tsx b/src/components/blog/CommentInputBlock.tsx index 8486ed6..5769d25 100644 --- a/src/components/blog/CommentInputBlock.tsx +++ b/src/components/blog/CommentInputBlock.tsx @@ -1,5 +1,6 @@ import { createEffect } from "solid-js"; import type { CommentInputBlockProps } from "~/types/comment"; +import Button from "~/components/ui/Button"; export default function CommentInputBlock(props: CommentInputBlockProps) { let bodyRef: HTMLTextAreaElement | undefined; @@ -37,17 +38,13 @@ export default function CommentInputBlock(props: CommentInputBlockProps) {
- +
diff --git a/src/components/blog/DeletePostButton.tsx b/src/components/blog/DeletePostButton.tsx index f7f327a..4880322 100644 --- a/src/components/blog/DeletePostButton.tsx +++ b/src/components/blog/DeletePostButton.tsx @@ -1,7 +1,7 @@ -import { createSignal, Show } from "solid-js"; +import { createSignal } from "solid-js"; import { api } from "~/lib/api"; import TrashIcon from "~/components/icons/TrashIcon"; -import LoadingSpinner from "~/components/LoadingSpinner"; +import Button from "~/components/ui/Button"; export interface DeletePostButtonProps { type: string; @@ -28,14 +28,14 @@ export default function DeletePostButton(props: DeletePostButtonProps) { return (
- +
); } diff --git a/src/components/blog/EditCommentModal.tsx b/src/components/blog/EditCommentModal.tsx index 324ee25..da224dc 100644 --- a/src/components/blog/EditCommentModal.tsx +++ b/src/components/blog/EditCommentModal.tsx @@ -1,6 +1,7 @@ import { createSignal, Show } from "solid-js"; import type { EditCommentModalProps } from "~/types/comment"; import Xmark from "~/components/icons/Xmark"; +import Button from "~/components/ui/Button"; export default function EditCommentModal(props: EditCommentModalProps) { let bodyRef: HTMLTextAreaElement | undefined; @@ -52,17 +53,13 @@ export default function EditCommentModal(props: EditCommentModalProps) {
- +
diff --git a/src/components/blog/PostForm.tsx b/src/components/blog/PostForm.tsx index 1c0321c..e08bd1e 100644 --- a/src/components/blog/PostForm.tsx +++ b/src/components/blog/PostForm.tsx @@ -9,6 +9,7 @@ import AddAttachmentSection from "~/components/blog/AddAttachmentSection"; import XCircle from "~/components/icons/XCircle"; import AddImageToS3 from "~/lib/s3upload"; import Input from "~/components/ui/Input"; +import Button from "~/components/ui/Button"; interface PostFormProps { mode: "create" | "edit"; @@ -542,23 +543,14 @@ export default function PostForm(props: PostFormProps) { {/* Submit button */}
- + {published() ? "Publish!" : "Save as Draft"} +
diff --git a/src/components/blog/TagMaker.tsx b/src/components/blog/TagMaker.tsx index 6ff0072..09cd163 100644 --- a/src/components/blog/TagMaker.tsx +++ b/src/components/blog/TagMaker.tsx @@ -1,6 +1,7 @@ import { For } from "solid-js"; import InfoIcon from "~/components/icons/InfoIcon"; import Xmark from "~/components/icons/Xmark"; +import IconButton from "~/components/ui/IconButton"; export interface TagMakerProps { tagInputValue: string; @@ -42,13 +43,20 @@ export default function TagMaker(props: TagMakerProps) {
{tag}
- + aria-label={`Remove tag ${tag}`} + variant="danger" + class="bg-mantle bg-opacity-50 absolute inset-0 flex items-center justify-center rounded-xl opacity-0 group-hover:opacity-100" + /> )} diff --git a/src/components/ui/AttachmentThumbnail.tsx b/src/components/ui/AttachmentThumbnail.tsx new file mode 100644 index 0000000..77da3a9 --- /dev/null +++ b/src/components/ui/AttachmentThumbnail.tsx @@ -0,0 +1,60 @@ +import { Show } from "solid-js"; +import type { JSX } from "solid-js"; +import IconButton from "./IconButton"; +import XCircle from "~/components/icons/XCircle"; + +export interface AttachmentThumbnailProps { + /** The URL of the file (either S3 URL or data URL) */ + fileUrl: string; + /** Whether the file is a video */ + isVideo: boolean; + /** Callback when the copy button is clicked */ + onCopy: () => void; + /** Callback when the remove button is clicked */ + onRemove: () => void; + /** Alt text for the image */ + alt?: string; + /** Additional CSS classes */ + class?: string; +} + +export default function AttachmentThumbnail(props: AttachmentThumbnailProps) { + return ( +
+ + } + onClick={props.onRemove} + aria-label={`Remove ${props.alt || "attachment"}`} + variant="danger" + class="hover:bg-crust hover:bg-opacity-80 absolute z-10 ml-4 pb-[120px]" + /> + +
+ ); +} + +export { AttachmentThumbnail }; diff --git a/src/components/ui/IconButton.tsx b/src/components/ui/IconButton.tsx new file mode 100644 index 0000000..7f6285d --- /dev/null +++ b/src/components/ui/IconButton.tsx @@ -0,0 +1,83 @@ +import { JSX, splitProps, Show } from "solid-js"; +import { Spinner } from "~/components/Spinner"; + +export interface IconButtonProps extends Omit< + JSX.ButtonHTMLAttributes, + "children" +> { + icon: JSX.Element; + "aria-label": string; + variant?: "ghost" | "danger" | "primary"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export default function IconButton(props: IconButtonProps) { + const [local, others] = splitProps(props, [ + "icon", + "aria-label", + "variant", + "size", + "loading", + "disabled", + "class" + ]); + + const variant = () => local.variant || "ghost"; + const size = () => local.size || "md"; + + const baseClasses = + "inline-flex items-center justify-center rounded transition-all duration-200 ease-out focus:outline-none focus-visible:ring-2 focus-visible:ring-blue focus-visible:ring-offset-2"; + + const variantClasses = () => { + const isDisabledOrLoading = local.disabled || local.loading; + + switch (variant()) { + case "ghost": + return isDisabledOrLoading + ? "cursor-not-allowed opacity-50" + : "text-text hover:bg-surface0/50 active:scale-95"; + case "danger": + return isDisabledOrLoading + ? "cursor-not-allowed opacity-50" + : "text-red hover:bg-red/10 active:scale-95"; + case "primary": + return isDisabledOrLoading + ? "cursor-not-allowed opacity-50" + : "text-blue hover:bg-blue/10 active:scale-95"; + default: + return ""; + } + }; + + const sizeClasses = () => { + switch (size()) { + case "sm": + return "p-1"; + case "md": + return "p-2"; + case "lg": + return "p-3"; + default: + return ""; + } + }; + + return ( + + ); +} + +export { IconButton };