singularity-components 0.1.196 → 0.1.197
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/blocks/cards/blogpost-card.js +1 -1
- package/dist/components/blocks/cards/blogpost-card.js.map +1 -1
- package/dist/components/blocks/directory/category-card.js +3 -2
- package/dist/components/blocks/directory/category-card.js.map +1 -1
- package/dist/components/blocks/extras/extras-hub-card.js +4 -3
- package/dist/components/blocks/extras/extras-hub-card.js.map +1 -1
- package/dist/components/blocks/login/login.js +76 -47
- package/dist/components/blocks/login/login.js.map +1 -1
- package/dist/components/blocks/marketing/timeline.js +2 -1
- package/dist/components/blocks/marketing/timeline.js.map +1 -1
- package/dist/components/blocks/post-list/post-list-with-filters.js +4 -4
- package/dist/components/blocks/post-list/post-list-with-filters.js.map +1 -1
- package/dist/components/pages/about/about-page.js +2 -2
- package/dist/components/pages/about/about-page.js.map +1 -1
- package/dist/components/pages/admin/admin-page.js +159 -105
- package/dist/components/pages/admin/admin-page.js.map +1 -1
- package/dist/components/pages/author/author-page.js +1 -1
- package/dist/components/pages/author/author-page.js.map +1 -1
- package/dist/components/pages/authors/authors-page.js +1 -1
- package/dist/components/pages/authors/authors-page.js.map +1 -1
- package/dist/components/pages/blogpost/blogpost.js +72 -44
- package/dist/components/pages/blogpost/blogpost.js.map +1 -1
- package/dist/components/pages/categories/categories-page.js +1 -1
- package/dist/components/pages/categories/categories-page.js.map +1 -1
- package/dist/components/pages/category/category-page.js +1 -1
- package/dist/components/pages/category/category-page.js.map +1 -1
- package/dist/components/pages/chat/chat-page.js +4 -4
- package/dist/components/pages/chat/chat-page.js.map +1 -1
- package/dist/components/pages/contact/contact-page.js +104 -97
- package/dist/components/pages/contact/contact-page.js.map +1 -1
- package/dist/components/pages/content-blocks/content-blocks-page.js +3 -2
- package/dist/components/pages/content-blocks/content-blocks-page.js.map +1 -1
- package/dist/components/pages/extras/extras-hub-page.js +1 -1
- package/dist/components/pages/extras/extras-hub-page.js.map +1 -1
- package/dist/components/pages/maintenance/maintenance-page.js +1 -1
- package/dist/components/pages/maintenance/maintenance-page.js.map +1 -1
- package/dist/components/pages/membership/membership-page.js +1 -1
- package/dist/components/pages/membership/membership-page.js.map +1 -1
- package/dist/components/pages/mosaic/mosaic-page.js +1 -1
- package/dist/components/pages/mosaic/mosaic-page.js.map +1 -1
- package/dist/components/pages/newsletter/newsletter-page.js +56 -39
- package/dist/components/pages/newsletter/newsletter-page.js.map +1 -1
- package/dist/components/pages/not-found/not-found.js +2 -2
- package/dist/components/pages/not-found/not-found.js.map +1 -1
- package/dist/components/pages/privacy/privacy-page.js +2 -2
- package/dist/components/pages/privacy/privacy-page.js.map +1 -1
- package/dist/components/pages/resources/resources-page.js +1 -1
- package/dist/components/pages/resources/resources-page.js.map +1 -1
- package/dist/components/pages/terms/terms-page.js +2 -2
- package/dist/components/pages/terms/terms-page.js.map +1 -1
- package/dist/components/primitives/forms/form.d.ts +1 -1
- package/dist/components/primitives/forms/form.js.map +1 -1
- package/dist/components/templates/form/form.d.ts +2 -2
- package/dist/components/templates/form/form.js +133 -87
- package/dist/components/templates/form/form.js.map +1 -1
- package/dist/components/templates/hero/hero.js +1 -0
- package/dist/components/templates/hero/hero.js.map +1 -1
- package/dist/components/templates/loading-screen/loading-screen.js +1 -1
- package/dist/components/templates/loading-screen/loading-screen.js.map +1 -1
- package/dist/css/variables.css +4 -3
- package/dist/css/variables.css.map +1 -1
- package/dist/data/posts.js +4 -4
- package/dist/data/posts.js.map +1 -1
- package/dist/lib/forms/index.d.ts +1 -1
- package/dist/lib/forms/tanstack-field.d.ts +26 -11
- package/dist/lib/forms/tanstack-field.js +13 -6
- package/dist/lib/forms/tanstack-field.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/main.css +16 -22
- package/dist/main.css.map +1 -1
- package/package.json +25 -30
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/pages/blogpost/blogpost.tsx"],"sourcesContent":["\"use client\";\nimport { useMemo, useState } from \"react\";\nimport DOMPurify from \"isomorphic-dompurify\";\nimport { mockComments, posts, type Comment } from \"../../../data/posts\";\nimport {\n Button,\n Layout,\n Link,\n TextTime,\n UiImage,\n Badges,\n Icon,\n Textarea,\n Input,\n Heading,\n Text,\n TextSpan,\n} from \"../../primitives/index\";\nimport { ArrowLeft, ArrowRight } from \"lucide-react\";\nimport EmptyState from \"../../blocks/empty-state/EmptyState\";\nimport CategoryBadge from \"../../blocks/badges/category-badge\";\nimport ImageGallery from \"../../blocks/gallery/image-gallery\";\nimport { useToast } from \"../../primitives/sonner/use-toast\";\nimport { authors } from \"../../../data/authors\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"../../primitives/avatar/avatar\";\nimport BlogPostCard from \"../../blocks/cards/blogpost-card\";\n\nfunction getReadTime(text: string): number {\n const words = text.trim().split(/\\s+/).length;\n return Math.max(1, Math.ceil(words / 200));\n}\n\ninterface BlogPostProps {\n slug?: string;\n}\n\nfunction BlogPost({ slug: propSlug = \"the-art-of-slow-living\" }: BlogPostProps) {\n const post = posts.find((p) => p.slug === propSlug);\n const postIndex = posts.findIndex((p) => p.slug === propSlug);\n const prevPost = postIndex > 0 ? posts[postIndex - 1] : null;\n const nextPost = postIndex < posts.length - 1 ? posts[postIndex + 1] : null;\n\n const isLoggedIn = true;\n const { toast } = useToast();\n\n const [comments, setComments] = useState<Comment[]>(mockComments);\n const [name, setName] = useState(\"\");\n const [text, setText] = useState(\"\");\n\n const [hasLiked, setHasLiked] = useState(false);\n const [likes, setLikes] = useState(0);\n\n const [isGenerating, setIsGenerating] = useState(false);\n const [isValidate, setIsValidate] = useState(false);\n\n const readTime = useMemo(\n () => (post ? getReadTime(post.content) : 0),\n [post],\n );\n\n const relatedPosts = useMemo(() => {\n if (!post) return [];\n return posts\n .filter(\n (p) =>\n p.id !== post.id &&\n p.categories.some((c) => post.categories.includes(c)),\n )\n .slice(0, 3);\n }, [post]);\n\n if (!post) {\n return (\n <Layout>\n <Layout.Col1 hideDiv>\n <EmptyState\n icon=\"BookOpen\"\n title=\"Post not found\"\n description=\"This post may have been removed or the link is incorrect.\"\n actionLabel=\"Browse all posts\"\n actionTo=\"/posts\"\n className=\"min-h-[60vh]\"\n />\n </Layout.Col1>\n </Layout>\n );\n }\n\n const handleLike = () => {\n if (!isLoggedIn) return;\n if (hasLiked) {\n setLikes((l) => l - 1);\n setHasLiked(false);\n } else {\n setLikes((l) => l + 1);\n setHasLiked(true);\n }\n };\n\n const handleGenerateImage = () => {\n setIsGenerating(true);\n setTimeout(() => {\n setIsGenerating(false);\n }, 1000);\n toast.message(\"Generating image…\", {\n description: \"A new AI image is being created for this post.\",\n dismissible: true,\n icon: <Icon icon=\"ImagePlus\" />,\n });\n };\n\n const handleValidate = () => {\n setIsValidate(true);\n setTimeout(() => {\n setIsValidate(false);\n }, 1000);\n toast.message(\"Post validated\", {\n description: \"Content has been reviewed.\",\n icon: <Icon icon=\"Check\" />,\n });\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!name.trim() || !text.trim()) return;\n setComments((prev) => [\n ...prev,\n {\n id: Date.now().toString(),\n author: name,\n text,\n date: new Date().toISOString().slice(0, 10),\n },\n ]);\n setName(\"\");\n setText(\"\");\n };\n\n // Simple markdown-ish rendering for content\n const renderBody = (body: string) => {\n return body.split(\"\\n\\n\").map((block, i) => {\n if (block.startsWith(\"## \")) {\n return (\n <Heading variant=\"h2\" key={i} className=\"sg:mt-8 sg:mb-4\">\n {block.replace(\"## \", \"\")}\n </Heading>\n );\n }\n if (block.startsWith(\"1. \") || block.startsWith(\"- \")) {\n const items = block\n .split(\"\\n\")\n .map((line) => line.replace(/^(\\d+\\.\\s|-\\s)/, \"\"));\n const isOrdered = block.startsWith(\"1.\");\n const Tag = isOrdered ? \"ol\" : \"ul\";\n\n return (\n <Tag\n key={i}\n className={`sg:my-4 sg:ml-6 sg:space-y-2 ${isOrdered ? \"sg:list-decimal\" : \"sg:list-disc\"}`}\n >\n {items.map((item, j) => (\n <li\n key={j}\n className=\"sg:text-muted-foreground\"\n dangerouslySetInnerHTML={{\n __html: DOMPurify.sanitize(\n item.replace(\n /\\*\\*(.*?)\\*\\*/g,\n '<strong class=\"text-foreground\">$1</strong>',\n ),\n ),\n }}\n />\n ))}\n </Tag>\n );\n }\n\n return (\n <Text\n key={i}\n className=\"sg:my-4\"\n foreground=\"muted-foreground\"\n dangerouslySetInnerHTML={{\n __html: DOMPurify.sanitize(\n block.replace(\n /\\*\\*(.*?)\\*\\*/g,\n '<strong class=\"sg:text-foreground\">$1</strong>',\n ),\n ),\n }}\n />\n );\n });\n };\n\n // Resolve author from post data\n const authorData = authors.find((a) => a.name === post?.author) ?? authors[0];\n\n return (\n <>\n <Layout type=\"container\">\n <Layout.Col1 className=\"sg:w-full sg:h-64 sg:md:h-96 sg:overflow-hidden\">\n <UiImage\n src={post.primaryImage || post.originalImage || \"\"}\n alt={post.title}\n className=\"sg:w-full sg:h-full sg:object-cover\"\n />\n </Layout.Col1>\n </Layout>\n <Layout type=\"col\" className=\"sg:py-12\" as=\"article\">\n <Layout.Col1 hideDiv>\n <div className=\"sg:flex sg:flex-col sg:gap-6 sg:items-start\">\n <Link to=\"/posts\" iconStart=\"ArrowLeft\">\n Back to all posts\n </Link>\n\n <TextTime size=\"sm\" foreground=\"muted-foreground\">\n {post.date} · {post.author} · {readTime} min read\n </TextTime>\n </div>\n\n <Heading variant=\"h1\" className=\"sg:mt-2\">\n {post.title}\n </Heading>\n\n {post.categories.length > 0 && (\n <Badges className=\"sg:mt-4\">\n {post.categories.map((cat) => (\n <CategoryBadge key={cat} category={cat} clickable />\n ))}\n </Badges>\n )}\n\n <Text\n size=\"lg\"\n foreground=\"muted-foreground\"\n className=\"sg:mt-4 sg:italic\"\n >\n {post.excerpt}\n </Text>\n\n {/* Like + admin actions bar */}\n <div className=\"sg:mt-6 sg:flex sg:items-center sg:gap-3 sg:flex-wrap\">\n <Button\n onClick={handleLike}\n disabled={!isLoggedIn}\n variant=\"outline\"\n size=\"sm\"\n iconStart=\"Heart\"\n iconStartFill={hasLiked}\n title={\n isLoggedIn\n ? hasLiked\n ? \"Unlike\"\n : \"Like this post\"\n : \"Log in to like\"\n }\n >\n {likes}\n </Button>\n\n {isLoggedIn && (\n <>\n <Button\n variant=\"outline\"\n iconStart=\"ImagePlus\"\n size=\"sm\"\n onClick={handleGenerateImage}\n loading={isGenerating}\n >\n Generate image\n </Button>\n <Button\n variant=\"outline\"\n iconStart=\"ShieldCheck\"\n size=\"sm\"\n onClick={handleValidate}\n loading={isValidate}\n >\n Validate\n </Button>\n </>\n )}\n </div>\n\n <div className=\"sg:mt-8 sg:border-t sg:pt-8\">\n {renderBody(post.content || \"\")}\n </div>\n\n {post.gallery && post.gallery.length > 0 && (\n <div className=\"sg:mt-12\">\n <ImageGallery images={post.gallery} />\n </div>\n )}\n\n {/* Secondary image */}\n <div className=\"sg:mt-12 sg:-mx-4 sg:md:-mx-8 sg:rounded-lg sg:overflow-hidden\">\n <UiImage\n src={(post.originalImage || \"\") + \"&q=80&crop=entropy\"}\n alt={`Visual for ${post.title}`}\n className=\"sg:w-full sg:h-56 sg:md:h-72 sg:object-cover\"\n loading=\"lazy\"\n />\n </div>\n\n {/* Author card */}\n {(() => {\n return (\n <div className=\"sg:mt-12 sg:border-t sg:pt-8\">\n <Link\n variant=\"no-decoration\"\n to={`/extras/authors/${authorData.slug}`}\n className=\"sg:group sg:flex sg:items-start sg:gap-4 sg:rounded-lg sg:border sg:bg-card sg:p-5 sg:hover:shadow-md sg:hover:border-primary/40 sg:transition-all\"\n >\n <Avatar className=\"sg:h-14 sg:w-14 sg:mt-0.5\">\n <AvatarImage\n src={authorData.avatar}\n alt={authorData.name}\n />\n <AvatarFallback>\n {authorData.name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n <div className=\"sg:min-w-0\">\n <TextSpan\n size=\"xs\"\n foreground=\"muted-foreground\"\n className=\"sg:uppercase sg:tracking-wider sg:mb-1\"\n >\n Written by\n </TextSpan>\n <Text\n size=\"sm\"\n fontweight=\"semibold\"\n className=\"sg:font-display sg:group-hover:text-primary sg:transition-colors\"\n >\n {authorData.name}\n </Text>\n <Text\n size=\"sm\"\n foreground=\"muted-foreground\"\n className=\"sg:mt-0.5\"\n >\n {authorData.role} · {authorData.location}\n </Text>\n <Text\n size=\"sm\"\n foreground=\"muted-foreground\"\n className=\"sg:mt-2 sg:line-clamp-2\"\n >\n {authorData.bio}\n </Text>\n </div>\n </Link>\n </div>\n );\n })()}\n\n {/* Prev / Next navigation */}\n <nav className=\"sg:mt-12 sg:border-t sg:pt-8 sg:grid sg:grid-cols-2 sg:gap-4\">\n {prevPost ? (\n <Link\n variant=\"no-decoration\"\n to={`/posts/${prevPost.slug}`}\n className=\"sg:group sg:flex sg:flex-col sg:items-start sg:gap-1 sg:rounded-lg sg:border sg:border-border sg:p-4 sg:hover:border-primary sg:transition-colors sg:text-right\"\n aria-label={`Previous post: ${prevPost.title}`}\n title={`Previous post: ${prevPost.title}`}\n >\n <TextSpan\n size=\"xs\"\n foreground=\"muted-foreground\"\n className=\"sg:flex sg:items-center sg:gap-1\"\n >\n <ArrowLeft className=\"h-3 w-3\" /> Previous\n </TextSpan>\n <TextSpan\n size=\"sm\"\n fontweight=\"medium\"\n className=\"sg:group-hover:text-primary sg:transition-colors sg:line-clamp-1 sg:text-start\"\n >\n {prevPost.title}\n </TextSpan>\n </Link>\n ) : (\n <div />\n )}\n {nextPost ? (\n <Link\n variant=\"no-decoration\"\n to={`/posts/${nextPost.slug}`}\n className=\"sg:group sg:flex sg:flex-col sg:items-end sg:gap-1 sg:rounded-lg sg:border sg:border-border sg:p-4 sg:hover:border-primary sg:transition-colors sg:text-right\"\n aria-label={`Next post: ${nextPost.title}`}\n title={`Next post: ${nextPost.title}`}\n >\n <TextSpan\n size=\"xs\"\n foreground=\"muted-foreground\"\n className=\"sg:flex sg:items-center sg:gap-1\"\n >\n Next <ArrowRight className=\"h-3 w-3\" />\n </TextSpan>\n <TextSpan\n size=\"sm\"\n fontweight=\"medium\"\n className=\"sg:group-hover:text-primary sg:transition-colors sg:line-clamp-1\"\n >\n {nextPost.title}\n </TextSpan>\n </Link>\n ) : (\n <div />\n )}\n </nav>\n\n {/* Comments */}\n <section className=\"sg:mt-16 sg:border-t sg:pt-8\">\n <Heading variant=\"h3\" className=\"sg:mb-6\">\n Comments ({comments.length})\n </Heading>\n\n <div className=\"sg:space-y-4 sg:mb-8\">\n {comments.length === 0 ? (\n <EmptyState\n icon=\"MessageSquare\"\n title=\"No comments yet\"\n description=\"Be the first to share your thoughts on this post.\"\n />\n ) : (\n comments.map((c) => (\n <div key={c.id} className=\"sg:rounded-lg sg:bg-accent sg:p-4\">\n <div className=\"sg:flex sg:items-center sg:gap-2 sg:mb-1\">\n <TextSpan fontweight=\"medium\" size=\"sm\">\n {c.author}\n </TextSpan>\n <TextSpan\n fontweight=\"medium\"\n size=\"xs\"\n foreground=\"muted-foreground\"\n >\n {c.date}\n </TextSpan>\n </div>\n <Text size=\"sm\" foreground=\"muted-foreground\">\n {c.text}\n </Text>\n </div>\n ))\n )}\n </div>\n\n <form onSubmit={handleSubmit} className=\"sg:space-y-4\">\n <Heading variant=\"h4\">Leave a comment</Heading>\n <Input\n placeholder=\"Your name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n required\n aria-label=\"Your name\"\n />\n <Textarea\n placeholder=\"Write your comment...\"\n value={text}\n onChange={(e) => setText(e.target.value)}\n required\n aria-label=\"Your comment\"\n />\n <Button type=\"submit\">Post comment</Button>\n </form>\n </section>\n\n {/* Related Posts */}\n {relatedPosts.length > 0 && (\n <section className=\"sg:mt-16 sg:border-t sg:pt-8\">\n <Heading variant=\"h3\" className=\"sg:mb-6\">\n Related Posts\n </Heading>\n <div className=\"sg:grid sg:gap-6 sg:sm:grid-cols-2 lg:grid-cols-3\">\n {relatedPosts.map((p) => (\n <BlogPostCard\n key={p.id}\n id={p.id}\n slug={p.slug}\n image={p.primaryImage}\n categories={p.categories}\n date={p.date}\n title={p.title}\n excerpt={p.excerpt}\n clickableCategories\n />\n ))}\n </div>\n </section>\n )}\n </Layout.Col1>\n </Layout>\n </>\n );\n}\n\nexport { BlogPost };\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/pages/blogpost/blogpost.tsx"],"sourcesContent":["\"use client\";\nimport { useForm } from \"@tanstack/react-form\";\nimport { useMemo, useState } from \"react\";\nimport DOMPurify from \"isomorphic-dompurify\";\nimport { mockComments, posts, type Comment } from \"../../../data/posts\";\nimport {\n\tTanStackInputField,\n\tTanStackTextareaField,\n} from \"../../../lib/forms/tanstack-field\";\nimport {\n Button,\n Layout,\n Link,\n TextTime,\n UiImage,\n Badges,\n Icon,\n Heading,\n Text,\n TextSpan,\n} from \"../../primitives/index\";\nimport { FieldGroup } from \"../../primitives/forms/field\";\nimport { Form, FormActions } from \"../../primitives/forms/form\";\nimport { ArrowLeft, ArrowRight } from \"lucide-react\";\nimport EmptyState from \"../../blocks/empty-state/EmptyState\";\nimport CategoryBadge from \"../../blocks/badges/category-badge\";\nimport ImageGallery from \"../../blocks/gallery/image-gallery\";\nimport { useToast } from \"../../primitives/sonner/use-toast\";\nimport { authors } from \"../../../data/authors\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"../../primitives/avatar/avatar\";\nimport BlogPostCard from \"../../blocks/cards/blogpost-card\";\n\nfunction getReadTime(text: string): number {\n const words = text.trim().split(/\\s+/).length;\n return Math.max(1, Math.ceil(words / 200));\n}\n\ninterface BlogPostProps {\n slug?: string;\n}\n\nfunction BlogPost({ slug: propSlug = \"the-art-of-slow-living\" }: BlogPostProps) {\n const post = posts.find((p) => p.slug === propSlug);\n const postIndex = posts.findIndex((p) => p.slug === propSlug);\n const prevPost = postIndex > 0 ? posts[postIndex - 1] : null;\n const nextPost = postIndex < posts.length - 1 ? posts[postIndex + 1] : null;\n\n const isLoggedIn = true;\n const { toast } = useToast();\n\n const [comments, setComments] = useState<Comment[]>(mockComments);\n\n const commentForm = useForm({\n defaultValues: { name: \"\", text: \"\" },\n onSubmit: ({ value, formApi }) => {\n setComments((prev) => [\n ...prev,\n {\n id: Date.now().toString(),\n author: value.name,\n text: value.text,\n date: new Date().toISOString().slice(0, 10),\n },\n ]);\n formApi.reset();\n },\n });\n\n const [hasLiked, setHasLiked] = useState(false);\n const [likes, setLikes] = useState(0);\n\n const [isGenerating, setIsGenerating] = useState(false);\n const [isValidate, setIsValidate] = useState(false);\n\n const readTime = useMemo(\n () => (post ? getReadTime(post.content) : 0),\n [post],\n );\n\n const relatedPosts = useMemo(() => {\n if (!post) return [];\n return posts\n .filter(\n (p) =>\n p.id !== post.id &&\n p.categories.some((c) => post.categories.includes(c)),\n )\n .slice(0, 3);\n }, [post]);\n\n if (!post) {\n return (\n <Layout>\n <Layout.Col1 hideDiv>\n <EmptyState\n icon=\"BookOpen\"\n title=\"Post not found\"\n description=\"This post may have been removed or the link is incorrect.\"\n actionLabel=\"Browse all posts\"\n actionTo=\"/posts\"\n className=\"min-h-[60vh]\"\n />\n </Layout.Col1>\n </Layout>\n );\n }\n\n const handleLike = () => {\n if (!isLoggedIn) return;\n if (hasLiked) {\n setLikes((l) => l - 1);\n setHasLiked(false);\n } else {\n setLikes((l) => l + 1);\n setHasLiked(true);\n }\n };\n\n const handleGenerateImage = () => {\n setIsGenerating(true);\n setTimeout(() => {\n setIsGenerating(false);\n }, 1000);\n toast.message(\"Generating image…\", {\n description: \"A new AI image is being created for this post.\",\n dismissible: true,\n icon: <Icon icon=\"ImagePlus\" />,\n });\n };\n\n const handleValidate = () => {\n setIsValidate(true);\n setTimeout(() => {\n setIsValidate(false);\n }, 1000);\n toast.message(\"Post validated\", {\n description: \"Content has been reviewed.\",\n icon: <Icon icon=\"Check\" />,\n });\n };\n\n // Simple markdown-ish rendering for content\n const renderBody = (body: string) => {\n return body.split(\"\\n\\n\").map((block, i) => {\n if (block.startsWith(\"## \")) {\n return (\n <Heading variant=\"h2\" key={i} className=\"sg:mt-8 sg:mb-4\">\n {block.replace(\"## \", \"\")}\n </Heading>\n );\n }\n if (block.startsWith(\"1. \") || block.startsWith(\"- \")) {\n const items = block\n .split(\"\\n\")\n .map((line) => line.replace(/^(\\d+\\.\\s|-\\s)/, \"\"));\n const isOrdered = block.startsWith(\"1.\");\n const Tag = isOrdered ? \"ol\" : \"ul\";\n\n return (\n <Tag\n key={i}\n className={`sg:my-4 sg:ml-6 sg:space-y-2 ${isOrdered ? \"sg:list-decimal\" : \"sg:list-disc\"}`}\n >\n {items.map((item, j) => (\n <li\n key={j}\n className=\"sg:text-muted-foreground\"\n dangerouslySetInnerHTML={{\n __html: DOMPurify.sanitize(\n item.replace(\n /\\*\\*(.*?)\\*\\*/g,\n '<strong class=\"text-foreground\">$1</strong>',\n ),\n ),\n }}\n />\n ))}\n </Tag>\n );\n }\n\n return (\n <Text\n key={i}\n className=\"sg:my-4\"\n foreground=\"muted-foreground\"\n dangerouslySetInnerHTML={{\n __html: DOMPurify.sanitize(\n block.replace(\n /\\*\\*(.*?)\\*\\*/g,\n '<strong class=\"sg:text-foreground\">$1</strong>',\n ),\n ),\n }}\n />\n );\n });\n };\n\n // Resolve author from post data\n const authorData = authors.find((a) => a.name === post?.author) ?? authors[0];\n\n return (\n <>\n <Layout type=\"container\">\n <Layout.Col1 className=\"sg:w-full sg:h-64 sg:md:h-96 sg:overflow-hidden\">\n <UiImage\n src={post.primaryImage || post.originalImage || \"\"}\n alt={post.title}\n className=\"sg:w-full sg:h-full sg:object-cover\"\n />\n </Layout.Col1>\n </Layout>\n <Layout type=\"col\" className=\"sg:py-12\" as=\"article\">\n <Layout.Col1 hideDiv>\n <div className=\"sg:flex sg:flex-col sg:gap-6 sg:items-start\">\n <Link to=\"/posts\" iconStart=\"ArrowLeft\">\n Back to all posts\n </Link>\n\n <TextTime size=\"sm\" foreground=\"muted-foreground\">\n {post.date} · {post.author} · {readTime} min read\n </TextTime>\n </div>\n\n <Heading variant=\"h1\" className=\"sg:mt-2\">\n {post.title}\n </Heading>\n\n {post.categories.length > 0 && (\n <Badges className=\"sg:mt-4\">\n {post.categories.map((cat) => (\n <CategoryBadge key={cat} category={cat} clickable />\n ))}\n </Badges>\n )}\n\n <Text\n size=\"lg\"\n foreground=\"muted-foreground\"\n className=\"sg:mt-4 sg:italic\"\n >\n {post.excerpt}\n </Text>\n\n {/* Like + admin actions bar */}\n <div className=\"sg:mt-6 sg:flex sg:items-center sg:gap-3 sg:flex-wrap\">\n <Button\n onClick={handleLike}\n disabled={!isLoggedIn}\n variant=\"outline\"\n size=\"sm\"\n iconStart=\"Heart\"\n iconStartFill={hasLiked}\n title={\n isLoggedIn\n ? hasLiked\n ? \"Unlike\"\n : \"Like this post\"\n : \"Log in to like\"\n }\n >\n {likes}\n </Button>\n\n {isLoggedIn && (\n <>\n <Button\n variant=\"outline\"\n iconStart=\"ImagePlus\"\n size=\"sm\"\n onClick={handleGenerateImage}\n loading={isGenerating}\n >\n Generate image\n </Button>\n <Button\n variant=\"outline\"\n iconStart=\"ShieldCheck\"\n size=\"sm\"\n onClick={handleValidate}\n loading={isValidate}\n >\n Validate\n </Button>\n </>\n )}\n </div>\n\n <div className=\"sg:mt-8 sg:border-t sg:pt-8\">\n {renderBody(post.content || \"\")}\n </div>\n\n {post.gallery && post.gallery.length > 0 && (\n <div className=\"sg:mt-12\">\n <ImageGallery images={post.gallery} />\n </div>\n )}\n\n {/* Secondary image */}\n <div className=\"sg:mt-12 sg:-mx-4 sg:md:-mx-8 sg:rounded-lg sg:overflow-hidden\">\n <UiImage\n src={(post.originalImage || \"\") + \"&q=80&crop=entropy\"}\n alt={`Visual for ${post.title}`}\n className=\"sg:w-full sg:h-56 sg:md:h-72 sg:object-cover\"\n loading=\"lazy\"\n />\n </div>\n\n {/* Author card */}\n {(() => {\n return (\n <div className=\"sg:mt-12 sg:border-t sg:pt-8\">\n <Link\n variant=\"no-decoration\"\n to={`/extras/authors/${authorData.slug}`}\n className=\"sg:group sg:flex sg:items-start sg:gap-4 sg:rounded-lg sg:border sg:bg-card sg:p-5 sg:hover:shadow-md sg:hover:border-primary/40 sg:transition-all\"\n >\n <Avatar className=\"sg:h-14 sg:w-14 sg:mt-0.5\">\n <AvatarImage\n src={authorData.avatar}\n alt={authorData.name}\n />\n <AvatarFallback>\n {authorData.name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n <div className=\"sg:min-w-0\">\n <TextSpan\n size=\"xs\"\n foreground=\"muted-foreground\"\n className=\"sg:uppercase sg:tracking-wider sg:mb-1\"\n >\n Written by\n </TextSpan>\n <Text\n size=\"sm\"\n fontweight=\"semibold\"\n className=\"sg:font-display sg:group-hover:text-primary sg:transition-colors\"\n >\n {authorData.name}\n </Text>\n <Text\n size=\"sm\"\n foreground=\"muted-foreground\"\n className=\"sg:mt-0.5\"\n >\n {authorData.role} · {authorData.location}\n </Text>\n <Text\n size=\"sm\"\n foreground=\"muted-foreground\"\n className=\"sg:mt-2 sg:line-clamp-2\"\n >\n {authorData.bio}\n </Text>\n </div>\n </Link>\n </div>\n );\n })()}\n\n {/* Prev / Next navigation */}\n <nav className=\"sg:mt-12 sg:border-t sg:pt-8 sg:grid sg:grid-cols-2 sg:gap-4\">\n {prevPost ? (\n <Link\n variant=\"no-decoration\"\n to={`/posts/${prevPost.slug}`}\n className=\"sg:group sg:flex sg:flex-col sg:items-start sg:gap-1 sg:rounded-lg sg:border sg:border-border sg:p-4 sg:hover:border-primary sg:transition-colors sg:text-right\"\n aria-label={`Previous post: ${prevPost.title}`}\n title={`Previous post: ${prevPost.title}`}\n >\n <TextSpan\n size=\"xs\"\n foreground=\"muted-foreground\"\n className=\"sg:flex sg:items-center sg:gap-1\"\n >\n <ArrowLeft className=\"h-3 w-3\" /> Previous\n </TextSpan>\n <TextSpan\n size=\"sm\"\n fontweight=\"medium\"\n className=\"sg:group-hover:text-primary sg:transition-colors sg:line-clamp-1 sg:text-start\"\n >\n {prevPost.title}\n </TextSpan>\n </Link>\n ) : (\n <div />\n )}\n {nextPost ? (\n <Link\n variant=\"no-decoration\"\n to={`/posts/${nextPost.slug}`}\n className=\"sg:group sg:flex sg:flex-col sg:items-end sg:gap-1 sg:rounded-lg sg:border sg:border-border sg:p-4 sg:hover:border-primary sg:transition-colors sg:text-right\"\n aria-label={`Next post: ${nextPost.title}`}\n title={`Next post: ${nextPost.title}`}\n >\n <TextSpan\n size=\"xs\"\n foreground=\"muted-foreground\"\n className=\"sg:flex sg:items-center sg:gap-1\"\n >\n Next <ArrowRight className=\"h-3 w-3\" />\n </TextSpan>\n <TextSpan\n size=\"sm\"\n fontweight=\"medium\"\n className=\"sg:group-hover:text-primary sg:transition-colors sg:line-clamp-1\"\n >\n {nextPost.title}\n </TextSpan>\n </Link>\n ) : (\n <div />\n )}\n </nav>\n\n {/* Comments */}\n <section className=\"sg:mt-16 sg:border-t sg:pt-8\">\n <Heading variant=\"h3\" className=\"sg:mb-6\">\n Comments ({comments.length})\n </Heading>\n\n <div className=\"sg:space-y-4 sg:mb-8\">\n {comments.length === 0 ? (\n <EmptyState\n icon=\"MessageSquare\"\n title=\"No comments yet\"\n description=\"Be the first to share your thoughts on this post.\"\n />\n ) : (\n comments.map((c) => (\n <div key={c.id} className=\"sg:rounded-lg sg:bg-accent sg:p-4\">\n <div className=\"sg:flex sg:items-center sg:gap-2 sg:mb-1\">\n <TextSpan fontweight=\"medium\" size=\"sm\">\n {c.author}\n </TextSpan>\n <TextSpan\n fontweight=\"medium\"\n size=\"xs\"\n foreground=\"muted-foreground\"\n >\n {c.date}\n </TextSpan>\n </div>\n <Text size=\"sm\" foreground=\"muted-foreground\">\n {c.text}\n </Text>\n </div>\n ))\n )}\n </div>\n\n <Form\n className=\"sg:gap-4\"\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void commentForm.handleSubmit();\n }}\n >\n <Heading variant=\"h4\">Leave a comment</Heading>\n <FieldGroup className=\"sg:gap-4\">\n <TanStackInputField\n formApi={commentForm}\n name=\"name\"\n label=\"Your name\"\n labelClassName=\"sg:sr-only\"\n placeholder=\"Your name\"\n validators={{\n onChange: ({ value }: { value: string }) =>\n value.trim() ? undefined : \"Name is required\",\n }}\n />\n <TanStackTextareaField\n formApi={commentForm}\n name=\"text\"\n label=\"Your comment\"\n labelClassName=\"sg:sr-only\"\n placeholder=\"Write your comment...\"\n validators={{\n onChange: ({ value }: { value: string }) =>\n value.trim() ? undefined : \"Comment is required\",\n }}\n />\n </FieldGroup>\n <FormActions>\n <commentForm.Subscribe\n selector={(state) => [state.canSubmit, state.isSubmitting]}\n >\n {([canSubmit, isSubmitting]) => (\n <Button type=\"submit\" disabled={!canSubmit || isSubmitting}>\n Post comment\n </Button>\n )}\n </commentForm.Subscribe>\n </FormActions>\n </Form>\n </section>\n\n {/* Related Posts */}\n {relatedPosts.length > 0 && (\n <section className=\"sg:mt-16 sg:border-t sg:pt-8\">\n <Heading variant=\"h3\" className=\"sg:mb-6\">\n Related Posts\n </Heading>\n <div className=\"sg:grid sg:gap-6 sg:sm:grid-cols-2 lg:sg:grid-cols-3\">\n {relatedPosts.map((p) => (\n <BlogPostCard\n key={p.id}\n id={p.id}\n slug={p.slug}\n image={p.primaryImage}\n categories={p.categories}\n date={p.date}\n title={p.title}\n excerpt={p.excerpt}\n clickableCategories\n />\n ))}\n </div>\n </section>\n )}\n </Layout.Col1>\n </Layout>\n </>\n );\n}\n\nexport { BlogPost };\n"],"mappings":"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","names":[]}
|
|
@@ -16,7 +16,7 @@ function CategoriesPage() {
|
|
|
16
16
|
return /* @__PURE__ */ jsx(Layout, { type: "col", className: "sg:py-16", children: /* @__PURE__ */ jsx(Layout.Col1, { hideDiv: true, className: "sg:max-w-3xl", children: /* @__PURE__ */ jsxs(
|
|
17
17
|
motion.div,
|
|
18
18
|
{
|
|
19
|
-
initial: { opacity:
|
|
19
|
+
initial: { opacity: 1, y: 20 },
|
|
20
20
|
animate: { opacity: 1, y: 0 },
|
|
21
21
|
transition: { duration: 0.3 },
|
|
22
22
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/pages/categories/categories-page.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport { useMemo } from \"react\";\r\nimport { motion } from \"framer-motion\";\r\nimport { Layout, Heading, Text } from \"../../primitives/index\";\r\nimport { CategoryCard } from \"../../blocks/directory/category-card\";\r\nimport { posts } from \"../../../data/posts\";\r\n\r\nexport function CategoriesPage() {\r\n\tconst categories = useMemo(() => {\r\n\t\tconst map = new Map<string, number>();\r\n\t\tposts.forEach((p) =>\r\n\t\t\tp.categories.forEach((c) => map.set(c, (map.get(c) || 0) + 1)),\r\n\t\t);\r\n\t\treturn Array.from(map.entries())\r\n\t\t\t.sort((a, b) => a[0].localeCompare(b[0]))\r\n\t\t\t.map(([name, count]) => ({ name, count }));\r\n\t}, []);\r\n\r\n\treturn (\r\n\t\t<Layout type=\"col\" className=\"sg:py-16\">\r\n\t\t\t<Layout.Col1 hideDiv className=\"sg:max-w-3xl\">\r\n\t\t\t\t<motion.div\r\n\t\t\t\t\tinitial={{ opacity:
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/pages/categories/categories-page.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport { useMemo } from \"react\";\r\nimport { motion } from \"framer-motion\";\r\nimport { Layout, Heading, Text } from \"../../primitives/index\";\r\nimport { CategoryCard } from \"../../blocks/directory/category-card\";\r\nimport { posts } from \"../../../data/posts\";\r\n\r\nexport function CategoriesPage() {\r\n\tconst categories = useMemo(() => {\r\n\t\tconst map = new Map<string, number>();\r\n\t\tposts.forEach((p) =>\r\n\t\t\tp.categories.forEach((c) => map.set(c, (map.get(c) || 0) + 1)),\r\n\t\t);\r\n\t\treturn Array.from(map.entries())\r\n\t\t\t.sort((a, b) => a[0].localeCompare(b[0]))\r\n\t\t\t.map(([name, count]) => ({ name, count }));\r\n\t}, []);\r\n\r\n\treturn (\r\n\t\t<Layout type=\"col\" className=\"sg:py-16\">\r\n\t\t\t<Layout.Col1 hideDiv className=\"sg:max-w-3xl\">\r\n\t\t\t\t<motion.div\r\n\t\t\t\t\tinitial={{ opacity: 1, y: 20 }}\r\n\t\t\t\t\tanimate={{ opacity: 1, y: 0 }}\r\n\t\t\t\t\ttransition={{ duration: 0.3 }}\r\n\t\t\t\t>\r\n\t\t\t\t\t<Heading variant=\"h1\" className=\"sg:mb-2\">\r\n\t\t\t\t\t\tCategories\r\n\t\t\t\t\t</Heading>\r\n\t\t\t\t\t<Text foreground=\"muted-foreground\" className=\"sg:mb-10\">\r\n\t\t\t\t\t\tBrowse posts by topic.\r\n\t\t\t\t\t</Text>\r\n\t\t\t\t\t<div className=\"sg:grid sg:gap-4 sm:sg:grid-cols-2\">\r\n\t\t\t\t\t\t{categories.map((cat) => (\r\n\t\t\t\t\t\t\t<CategoryCard key={cat.name} name={cat.name} count={cat.count} />\r\n\t\t\t\t\t\t))}\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</motion.div>\r\n\t\t\t</Layout.Col1>\r\n\t\t</Layout>\r\n\t);\r\n}\r\n"],"mappings":";AAsBI,SAKC,KALD;AApBJ,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,QAAQ,SAAS,YAAY;AACtC,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AAEf,SAAS,iBAAiB;AAChC,QAAM,aAAa,QAAQ,MAAM;AAChC,UAAM,MAAM,oBAAI,IAAoB;AACpC,UAAM;AAAA,MAAQ,CAAC,MACd,EAAE,WAAW,QAAQ,CAAC,MAAM,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC;AAAA,IAC9D;AACA,WAAO,MAAM,KAAK,IAAI,QAAQ,CAAC,EAC7B,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EACvC,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO,EAAE,MAAM,MAAM,EAAE;AAAA,EAC3C,GAAG,CAAC,CAAC;AAEL,SACC,oBAAC,UAAO,MAAK,OAAM,WAAU,YAC5B,8BAAC,OAAO,MAAP,EAAY,SAAO,MAAC,WAAU,gBAC9B;AAAA,IAAC,OAAO;AAAA,IAAP;AAAA,MACA,SAAS,EAAE,SAAS,GAAG,GAAG,GAAG;AAAA,MAC7B,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE;AAAA,MAC5B,YAAY,EAAE,UAAU,IAAI;AAAA,MAE5B;AAAA,4BAAC,WAAQ,SAAQ,MAAK,WAAU,WAAU,wBAE1C;AAAA,QACA,oBAAC,QAAK,YAAW,oBAAmB,WAAU,YAAW,oCAEzD;AAAA,QACA,oBAAC,SAAI,WAAU,sCACb,qBAAW,IAAI,CAAC,QAChB,oBAAC,gBAA4B,MAAM,IAAI,MAAM,OAAO,IAAI,SAArC,IAAI,IAAwC,CAC/D,GACF;AAAA;AAAA;AAAA,EACD,GACD,GACD;AAEF;","names":[]}
|
|
@@ -30,7 +30,7 @@ function CategoryPage({ category: propCategory }) {
|
|
|
30
30
|
return /* @__PURE__ */ jsx(Layout, { type: "col", className: "sg:py-16", as: "main", children: /* @__PURE__ */ jsx(Layout.Col1, { hideDiv: true, children: /* @__PURE__ */ jsxs(
|
|
31
31
|
motion.div,
|
|
32
32
|
{
|
|
33
|
-
initial: { opacity:
|
|
33
|
+
initial: { opacity: 1, y: 20 },
|
|
34
34
|
animate: { opacity: 1, y: 0 },
|
|
35
35
|
transition: { duration: 0.3 },
|
|
36
36
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/pages/category/category-page.tsx"],"sourcesContent":["\"use client\";\nimport { useMemo } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Layout, Heading, Text } from \"../../primitives/index\";\nimport { LinkButton } from \"../../primitives/buttons/link-button\";\nimport { posts } from \"../../../data/posts\";\nimport { PostListWithFilters } from \"../../blocks/post-list/post-list-with-filters\";\nimport EmptyState from \"../../blocks/empty-state/EmptyState\";\n\ninterface Props {\n category?: string;\n}\n\nexport function CategoryPage({ category: propCategory }: Props) {\n // In a real app, this would come from useParams()\n // For the library/storybook, we allow passing it as a prop\n const category = propCategory || \"Design\";\n const decodedCategory = decodeURIComponent(category);\n\n const categoryPosts = useMemo(\n () => posts.filter((p) => p.categories.includes(decodedCategory)),\n [decodedCategory],\n );\n\n if (!decodedCategory || categoryPosts.length === 0) {\n return (\n <Layout type=\"col\" className=\"sg:py-16\">\n <Layout.Col1 hideDiv>\n <EmptyState\n icon=\"Tag\"\n title=\"Category not found\"\n description={\n decodedCategory\n ? `No posts found in \"${decodedCategory}\". It may have been removed or renamed.`\n : \"This category doesn't exist.\"\n }\n actionLabel=\"Browse all posts\"\n actionTo=\"/posts\"\n className=\"sg:min-h-[60vh]\"\n />\n </Layout.Col1>\n </Layout>\n );\n }\n\n return (\n <Layout type=\"col\" className=\"sg:py-16\" as=\"main\">\n <Layout.Col1 hideDiv>\n <motion.div\n initial={{ opacity:
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/pages/category/category-page.tsx"],"sourcesContent":["\"use client\";\nimport { useMemo } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Layout, Heading, Text } from \"../../primitives/index\";\nimport { LinkButton } from \"../../primitives/buttons/link-button\";\nimport { posts } from \"../../../data/posts\";\nimport { PostListWithFilters } from \"../../blocks/post-list/post-list-with-filters\";\nimport EmptyState from \"../../blocks/empty-state/EmptyState\";\n\ninterface Props {\n category?: string;\n}\n\nexport function CategoryPage({ category: propCategory }: Props) {\n // In a real app, this would come from useParams()\n // For the library/storybook, we allow passing it as a prop\n const category = propCategory || \"Design\";\n const decodedCategory = decodeURIComponent(category);\n\n const categoryPosts = useMemo(\n () => posts.filter((p) => p.categories.includes(decodedCategory)),\n [decodedCategory],\n );\n\n if (!decodedCategory || categoryPosts.length === 0) {\n return (\n <Layout type=\"col\" className=\"sg:py-16\">\n <Layout.Col1 hideDiv>\n <EmptyState\n icon=\"Tag\"\n title=\"Category not found\"\n description={\n decodedCategory\n ? `No posts found in \"${decodedCategory}\". It may have been removed or renamed.`\n : \"This category doesn't exist.\"\n }\n actionLabel=\"Browse all posts\"\n actionTo=\"/posts\"\n className=\"sg:min-h-[60vh]\"\n />\n </Layout.Col1>\n </Layout>\n );\n }\n\n return (\n <Layout type=\"col\" className=\"sg:py-16\" as=\"main\">\n <Layout.Col1 hideDiv>\n <motion.div\n initial={{ opacity: 1, y: 20 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ duration: 0.3 }}\n >\n <LinkButton\n variant=\"ghost\"\n size=\"sm\"\n to=\"/posts\"\n iconStart=\"ArrowLeft\"\n className=\"sg:mb-6\"\n >\n All Posts\n </LinkButton>\n\n <Heading variant=\"h1\" className=\"sg:mb-2 sg:capitalize\">\n {decodedCategory}\n </Heading>\n <Text foreground=\"muted-foreground\" className=\"sg:mb-12\">\n Explore {categoryPosts.length}{\" \"}\n {categoryPosts.length === 1 ? \"story\" : \"stories\"} in this category.\n </Text>\n\n <PostListWithFilters\n posts={categoryPosts}\n hideSearch\n filterMode=\"drawer\"\n />\n </motion.div>\n </Layout.Col1>\n </Layout>\n );\n}\n"],"mappings":";AA4BU,cAsCA,YAtCA;AA3BV,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,QAAQ,SAAS,YAAY;AACtC,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AACtB,SAAS,2BAA2B;AACpC,OAAO,gBAAgB;AAMhB,SAAS,aAAa,EAAE,UAAU,aAAa,GAAU;AAG9D,QAAM,WAAW,gBAAgB;AACjC,QAAM,kBAAkB,mBAAmB,QAAQ;AAEnD,QAAM,gBAAgB;AAAA,IACpB,MAAM,MAAM,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS,eAAe,CAAC;AAAA,IAChE,CAAC,eAAe;AAAA,EAClB;AAEA,MAAI,CAAC,mBAAmB,cAAc,WAAW,GAAG;AAClD,WACE,oBAAC,UAAO,MAAK,OAAM,WAAU,YAC3B,8BAAC,OAAO,MAAP,EAAY,SAAO,MAClB;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,aACE,kBACI,sBAAsB,eAAe,4CACrC;AAAA,QAEN,aAAY;AAAA,QACZ,UAAS;AAAA,QACT,WAAU;AAAA;AAAA,IACZ,GACF,GACF;AAAA,EAEJ;AAEA,SACE,oBAAC,UAAO,MAAK,OAAM,WAAU,YAAW,IAAG,QACzC,8BAAC,OAAO,MAAP,EAAY,SAAO,MAClB;AAAA,IAAC,OAAO;AAAA,IAAP;AAAA,MACC,SAAS,EAAE,SAAS,GAAG,GAAG,GAAG;AAAA,MAC7B,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE;AAAA,MAC5B,YAAY,EAAE,UAAU,IAAI;AAAA,MAE5B;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,IAAG;AAAA,YACH,WAAU;AAAA,YACV,WAAU;AAAA,YACX;AAAA;AAAA,QAED;AAAA,QAEA,oBAAC,WAAQ,SAAQ,MAAK,WAAU,yBAC7B,2BACH;AAAA,QACA,qBAAC,QAAK,YAAW,oBAAmB,WAAU,YAAW;AAAA;AAAA,UAC9C,cAAc;AAAA,UAAQ;AAAA,UAC9B,cAAc,WAAW,IAAI,UAAU;AAAA,UAAU;AAAA,WACpD;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,YAAU;AAAA,YACV,YAAW;AAAA;AAAA,QACb;AAAA;AAAA;AAAA,EACF,GACF,GACF;AAEJ;","names":[]}
|
|
@@ -74,9 +74,9 @@ function ChatPage() {
|
|
|
74
74
|
return /* @__PURE__ */ jsx(Layout, { type: "container", bgColor: "background", className: "sg:min-h-screen", children: /* @__PURE__ */ jsx(Layout.Col1, { className: "sg:flex sg:flex-col sg:items-center", children: /* @__PURE__ */ jsxs(
|
|
75
75
|
motion.div,
|
|
76
76
|
{
|
|
77
|
-
initial: { opacity:
|
|
77
|
+
initial: { opacity: 1, y: 20 },
|
|
78
78
|
animate: { opacity: 1, y: 0 },
|
|
79
|
-
exit: { opacity:
|
|
79
|
+
exit: { opacity: 1, y: -20 },
|
|
80
80
|
transition: { duration: 0.3 },
|
|
81
81
|
className: "sg:container sg:py-8 sg:flex sg:flex-col sg:max-w-2xl sg:mx-auto sg:w-full",
|
|
82
82
|
style: { height: "calc(100vh - 2rem)" },
|
|
@@ -97,7 +97,7 @@ function ChatPage() {
|
|
|
97
97
|
messages.map((msg) => /* @__PURE__ */ jsxs(
|
|
98
98
|
motion.div,
|
|
99
99
|
{
|
|
100
|
-
initial: { opacity:
|
|
100
|
+
initial: { opacity: 1, y: 10 },
|
|
101
101
|
animate: { opacity: 1, y: 0 },
|
|
102
102
|
transition: { duration: 0.2 },
|
|
103
103
|
className: cn(
|
|
@@ -147,7 +147,7 @@ function ChatPage() {
|
|
|
147
147
|
isTyping && /* @__PURE__ */ jsxs(
|
|
148
148
|
motion.div,
|
|
149
149
|
{
|
|
150
|
-
initial: { opacity:
|
|
150
|
+
initial: { opacity: 1, y: 10 },
|
|
151
151
|
animate: { opacity: 1, y: 0 },
|
|
152
152
|
className: "sg:flex sg:gap-3",
|
|
153
153
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/pages/chat/chat-page.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, useRef, useEffect } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Send, Bot, User, Sparkles } from \"lucide-react\";\nimport { Layout, Button, Input, Heading, Text } from \"../../primitives/index\";\nimport { cn } from \"../../../utils/index\";\n\ninterface Message {\n id: string;\n role: \"user\" | \"assistant\";\n content: string;\n timestamp: Date;\n}\n\nconst mockResponses: Record<string, string> = {\n hello:\n \"Hey there! 👋 I'm **Storied Bot**, your friendly AI assistant. I can help you explore blog posts, find resources, or just chat. What's on your mind?\",\n help: \"Sure! Here's what I can help with:\\n\\n- 🔍 **Search posts** — Ask me about any topic\\n- 📚 **Recommend reads** — I'll suggest posts based on your interests\\n- 🎨 **Design tips** — Questions about typography, color, layout\\n- 💡 **General chat** — I'm happy to talk about anything!\",\n posts:\n \"We have some great posts! Here are a few highlights:\\n\\n1. **The Art of Minimalism** — A deep dive into less-is-more design\\n2. **Color Theory Fundamentals** — Understanding how colors work together\\n3. **Typography Best Practices** — Making your text shine\\n\\nWant me to go deeper on any of these?\",\n design:\n \"Great question about design! Here are some key principles I follow:\\n\\n- **Hierarchy** — Guide the eye with size, weight, and contrast\\n- **Whitespace** — Give elements room to breathe\\n- **Consistency** — Stick to your design system\\n- **Accessibility** — Make sure everyone can use your work\\n\\nWould you like to explore our **Design System** page for live examples?\",\n};\n\nconst defaultResponse =\n \"That's an interesting thought! 🤔\\n\\nI'm currently running in demo mode, so my responses are limited. In a full implementation, I'd be powered by an AI model and could have much richer conversations.\\n\\nTry asking me about **posts**, **design**, or type **help** to see what I can do!\";\n\nfunction getResponse(input: string): string {\n const lower = input.toLowerCase().trim();\n for (const [key, response] of Object.entries(mockResponses)) {\n if (lower.includes(key)) return response;\n }\n return defaultResponse;\n}\n\nconst initialMessages: Message[] = [\n {\n id: \"welcome\",\n role: \"assistant\",\n content:\n \"Welcome to **Storied Bot**! 🤖✨\\n\\nI'm your AI assistant — here to help you explore content, answer questions, and chat. This is a demo showcasing how a conversational interface can look and feel.\\n\\nTry saying **hello**, ask about **posts**, or type **help** to get started!\",\n timestamp: new Date(Date.now() - 60000),\n },\n];\n\nexport function ChatPage() {\n const [messages, setMessages] = useState<Message[]>(initialMessages);\n const [input, setInput] = useState(\"\");\n const [isTyping, setIsTyping] = useState(false);\n const scrollRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n scrollRef.current?.scrollTo({\n top: scrollRef.current.scrollHeight,\n behavior: \"smooth\",\n });\n }, [messages, isTyping]);\n\n const sendMessage = () => {\n const text = input.trim();\n if (!text || isTyping) return;\n\n const userMsg: Message = {\n id: crypto.randomUUID(),\n role: \"user\",\n content: text,\n timestamp: new Date(),\n };\n\n setMessages((prev) => [...prev, userMsg]);\n setInput(\"\");\n setIsTyping(true);\n\n // Simulate typing delay\n setTimeout(\n () => {\n const botMsg: Message = {\n id: crypto.randomUUID(),\n role: \"assistant\",\n content: getResponse(text),\n timestamp: new Date(),\n };\n setMessages((prev) => [...prev, botMsg]);\n setIsTyping(false);\n },\n 800 + Math.random() * 1200,\n );\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n sendMessage();\n }\n };\n\n return (\n <Layout type=\"container\" bgColor=\"background\" className=\"sg:min-h-screen\">\n <Layout.Col1 className=\"sg:flex sg:flex-col sg:items-center\">\n <motion.div\n initial={{ opacity: 0, y: 20 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -20 }}\n transition={{ duration: 0.3 }}\n className=\"sg:container sg:py-8 sg:flex sg:flex-col sg:max-w-2xl sg:mx-auto sg:w-full\"\n style={{ height: \"calc(100vh - 2rem)\" }}\n >\n {/* Header */}\n <div className=\"sg:flex sg:items-center sg:gap-3 sg:mb-4 sg:pb-4 sg:border-b sg:border-border\">\n <div className=\"sg:h-10 sg:w-10 sg:rounded-full sg:bg-primary/10 sg:flex sg:items-center sg:justify-center\">\n <Sparkles className=\"sg:h-5 sg:w-5 sg:text-primary\" />\n </div>\n <div>\n <Heading variant=\"h4\" className=\"sg:font-bold sg:leading-tight\">\n Storied Bot\n </Heading>\n <Text className=\"sg:text-xs sg:text-muted-foreground\">\n AI assistant · Demo mode\n </Text>\n </div>\n </div>\n\n {/* Messages area */}\n <div\n ref={scrollRef}\n className=\"chat-scroll sg:flex-1 sg:overflow-y-auto sg:space-y-4 sg:pr-2 sg:min-h-0\"\n >\n {messages.map((msg) => (\n <motion.div\n key={msg.id}\n initial={{ opacity: 0, y: 10 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ duration: 0.2 }}\n className={cn(\n \"sg:flex sg:gap-3 sg:max-w-[85%]\",\n msg.role === \"user\" ? \"sg:ml-auto sg:flex-row-reverse\" : \"\",\n )}\n >\n {/* Avatar */}\n <div\n className={cn(\n \"sg:h-8 sg:w-8 sg:rounded-full sg:flex sg:items-center sg:justify-center sg:shrink-0 sg:mt-0.5\",\n msg.role === \"assistant\"\n ? \"sg:bg-primary/10 sg:text-primary\"\n : \"sg:bg-secondary sg:text-secondary-foreground\",\n )}\n >\n {msg.role === \"assistant\" ? (\n <Bot className=\"sg:h-4 sg:w-4\" />\n ) : (\n <User className=\"sg:h-4 sg:w-4\" />\n )}\n </div>\n\n {/* Bubble */}\n <div\n className={cn(\n \"sg:rounded-2xl sg:px-4 sg:py-2.5 sg:text-sm sg:leading-relaxed\",\n msg.role === \"assistant\"\n ? \"sg:bg-muted sg:text-foreground sg:rounded-tl-sm\"\n : \"sg:bg-primary sg:text-primary-foreground sg:rounded-tr-sm\",\n )}\n >\n <MessageContent content={msg.content} />\n <p\n className={cn(\n \"sg:text-[10px] sg:mt-1.5\",\n msg.role === \"assistant\"\n ? \"sg:text-muted-foreground\"\n : \"sg:text-primary-foreground/60\",\n )}\n >\n {msg.timestamp.toLocaleTimeString([], {\n hour: \"2-digit\",\n minute: \"2-digit\",\n })}\n </p>\n </div>\n </motion.div>\n ))}\n\n {/* Typing indicator */}\n {isTyping && (\n <motion.div\n initial={{ opacity: 0, y: 10 }}\n animate={{ opacity: 1, y: 0 }}\n className=\"sg:flex sg:gap-3\"\n >\n <div className=\"sg:h-8 sg:w-8 sg:rounded-full sg:bg-primary/10 sg:text-primary sg:flex sg:items-center sg:justify-center sg:shrink-0\">\n <Bot className=\"sg:h-4 sg:w-4\" />\n </div>\n <div className=\"sg:bg-muted sg:rounded-2xl sg:rounded-tl-sm sg:px-4 sg:py-3 sg:flex sg:items-center sg:gap-1\">\n <span className=\"sg:w-2 sg:h-2 sg:rounded-full sg:bg-muted-foreground/40 sg:animate-bounce [animation-delay:0ms]\" />\n <span className=\"sg:w-2 sg:h-2 sg:rounded-full sg:bg-muted-foreground/40 sg:animate-bounce [animation-delay:150ms]\" />\n <span className=\"sg:w-2 sg:h-2 sg:rounded-full sg:bg-muted-foreground/40 sg:animate-bounce [animation-delay:300ms]\" />\n </div>\n </motion.div>\n )}\n </div>\n\n {/* Input area */}\n <div className=\"sg:pt-4 sg:mt-4 sg:border-t sg:border-border\">\n <div className=\"sg:flex sg:gap-2\">\n <Input\n ref={inputRef}\n placeholder=\"Type a message…\"\n value={input}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={handleKeyDown}\n disabled={isTyping}\n className=\"sg:flex-1\"\n />\n <Button\n onClick={sendMessage}\n disabled={!input.trim() || isTyping}\n aria-label=\"Send message\"\n >\n <Send className=\"sg:h-4 sg:w-4\" />\n </Button>\n </div>\n <Text className=\"sg:text-[11px] sg:text-muted-foreground sg:text-center sg:mt-2\">\n Demo mode — responses are pre-written. Try: hello, help, posts,\n design\n </Text>\n </div>\n </motion.div>\n </Layout.Col1>\n </Layout>\n );\n}\n\n/** Simple markdown-ish renderer for bold and line breaks */\nfunction MessageContent({ content }: { content: string }) {\n const lines = content.split(\"\\n\");\n return (\n <div className=\"sg:space-y-1.5\">\n {lines.map((line, i) => {\n if (line.trim() === \"\") return <br key={i} />;\n // Bold: **text**\n const parts = line.split(/(\\*\\*[^*]+\\*\\*)/g);\n return (\n <p key={i}>\n {parts.map((part, j) =>\n part.startsWith(\"**\") && part.endsWith(\"**\") ? (\n <strong key={j} className=\"sg:font-semibold\">\n {part.slice(2, -2)}\n </strong>\n ) : (\n <span key={j}>{part}</span>\n ),\n )}\n </p>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/pages/chat/chat-page.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, useRef, useEffect } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { Send, Bot, User, Sparkles } from \"lucide-react\";\nimport { Layout, Button, Input, Heading, Text } from \"../../primitives/index\";\nimport { cn } from \"../../../utils/index\";\n\ninterface Message {\n id: string;\n role: \"user\" | \"assistant\";\n content: string;\n timestamp: Date;\n}\n\nconst mockResponses: Record<string, string> = {\n hello:\n \"Hey there! 👋 I'm **Storied Bot**, your friendly AI assistant. I can help you explore blog posts, find resources, or just chat. What's on your mind?\",\n help: \"Sure! Here's what I can help with:\\n\\n- 🔍 **Search posts** — Ask me about any topic\\n- 📚 **Recommend reads** — I'll suggest posts based on your interests\\n- 🎨 **Design tips** — Questions about typography, color, layout\\n- 💡 **General chat** — I'm happy to talk about anything!\",\n posts:\n \"We have some great posts! Here are a few highlights:\\n\\n1. **The Art of Minimalism** — A deep dive into less-is-more design\\n2. **Color Theory Fundamentals** — Understanding how colors work together\\n3. **Typography Best Practices** — Making your text shine\\n\\nWant me to go deeper on any of these?\",\n design:\n \"Great question about design! Here are some key principles I follow:\\n\\n- **Hierarchy** — Guide the eye with size, weight, and contrast\\n- **Whitespace** — Give elements room to breathe\\n- **Consistency** — Stick to your design system\\n- **Accessibility** — Make sure everyone can use your work\\n\\nWould you like to explore our **Design System** page for live examples?\",\n};\n\nconst defaultResponse =\n \"That's an interesting thought! 🤔\\n\\nI'm currently running in demo mode, so my responses are limited. In a full implementation, I'd be powered by an AI model and could have much richer conversations.\\n\\nTry asking me about **posts**, **design**, or type **help** to see what I can do!\";\n\nfunction getResponse(input: string): string {\n const lower = input.toLowerCase().trim();\n for (const [key, response] of Object.entries(mockResponses)) {\n if (lower.includes(key)) return response;\n }\n return defaultResponse;\n}\n\nconst initialMessages: Message[] = [\n {\n id: \"welcome\",\n role: \"assistant\",\n content:\n \"Welcome to **Storied Bot**! 🤖✨\\n\\nI'm your AI assistant — here to help you explore content, answer questions, and chat. This is a demo showcasing how a conversational interface can look and feel.\\n\\nTry saying **hello**, ask about **posts**, or type **help** to get started!\",\n timestamp: new Date(Date.now() - 60000),\n },\n];\n\nexport function ChatPage() {\n const [messages, setMessages] = useState<Message[]>(initialMessages);\n const [input, setInput] = useState(\"\");\n const [isTyping, setIsTyping] = useState(false);\n const scrollRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n scrollRef.current?.scrollTo({\n top: scrollRef.current.scrollHeight,\n behavior: \"smooth\",\n });\n }, [messages, isTyping]);\n\n const sendMessage = () => {\n const text = input.trim();\n if (!text || isTyping) return;\n\n const userMsg: Message = {\n id: crypto.randomUUID(),\n role: \"user\",\n content: text,\n timestamp: new Date(),\n };\n\n setMessages((prev) => [...prev, userMsg]);\n setInput(\"\");\n setIsTyping(true);\n\n // Simulate typing delay\n setTimeout(\n () => {\n const botMsg: Message = {\n id: crypto.randomUUID(),\n role: \"assistant\",\n content: getResponse(text),\n timestamp: new Date(),\n };\n setMessages((prev) => [...prev, botMsg]);\n setIsTyping(false);\n },\n 800 + Math.random() * 1200,\n );\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n sendMessage();\n }\n };\n\n return (\n <Layout type=\"container\" bgColor=\"background\" className=\"sg:min-h-screen\">\n <Layout.Col1 className=\"sg:flex sg:flex-col sg:items-center\">\n <motion.div\n initial={{ opacity: 1, y: 20 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 1, y: -20 }}\n transition={{ duration: 0.3 }}\n className=\"sg:container sg:py-8 sg:flex sg:flex-col sg:max-w-2xl sg:mx-auto sg:w-full\"\n style={{ height: \"calc(100vh - 2rem)\" }}\n >\n {/* Header */}\n <div className=\"sg:flex sg:items-center sg:gap-3 sg:mb-4 sg:pb-4 sg:border-b sg:border-border\">\n <div className=\"sg:h-10 sg:w-10 sg:rounded-full sg:bg-primary/10 sg:flex sg:items-center sg:justify-center\">\n <Sparkles className=\"sg:h-5 sg:w-5 sg:text-primary\" />\n </div>\n <div>\n <Heading variant=\"h4\" className=\"sg:font-bold sg:leading-tight\">\n Storied Bot\n </Heading>\n <Text className=\"sg:text-xs sg:text-muted-foreground\">\n AI assistant · Demo mode\n </Text>\n </div>\n </div>\n\n {/* Messages area */}\n <div\n ref={scrollRef}\n className=\"chat-scroll sg:flex-1 sg:overflow-y-auto sg:space-y-4 sg:pr-2 sg:min-h-0\"\n >\n {messages.map((msg) => (\n <motion.div\n key={msg.id}\n initial={{ opacity: 1, y: 10 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ duration: 0.2 }}\n className={cn(\n \"sg:flex sg:gap-3 sg:max-w-[85%]\",\n msg.role === \"user\" ? \"sg:ml-auto sg:flex-row-reverse\" : \"\",\n )}\n >\n {/* Avatar */}\n <div\n className={cn(\n \"sg:h-8 sg:w-8 sg:rounded-full sg:flex sg:items-center sg:justify-center sg:shrink-0 sg:mt-0.5\",\n msg.role === \"assistant\"\n ? \"sg:bg-primary/10 sg:text-primary\"\n : \"sg:bg-secondary sg:text-secondary-foreground\",\n )}\n >\n {msg.role === \"assistant\" ? (\n <Bot className=\"sg:h-4 sg:w-4\" />\n ) : (\n <User className=\"sg:h-4 sg:w-4\" />\n )}\n </div>\n\n {/* Bubble */}\n <div\n className={cn(\n \"sg:rounded-2xl sg:px-4 sg:py-2.5 sg:text-sm sg:leading-relaxed\",\n msg.role === \"assistant\"\n ? \"sg:bg-muted sg:text-foreground sg:rounded-tl-sm\"\n : \"sg:bg-primary sg:text-primary-foreground sg:rounded-tr-sm\",\n )}\n >\n <MessageContent content={msg.content} />\n <p\n className={cn(\n \"sg:text-[10px] sg:mt-1.5\",\n msg.role === \"assistant\"\n ? \"sg:text-muted-foreground\"\n : \"sg:text-primary-foreground/60\",\n )}\n >\n {msg.timestamp.toLocaleTimeString([], {\n hour: \"2-digit\",\n minute: \"2-digit\",\n })}\n </p>\n </div>\n </motion.div>\n ))}\n\n {/* Typing indicator */}\n {isTyping && (\n <motion.div\n initial={{ opacity: 1, y: 10 }}\n animate={{ opacity: 1, y: 0 }}\n className=\"sg:flex sg:gap-3\"\n >\n <div className=\"sg:h-8 sg:w-8 sg:rounded-full sg:bg-primary/10 sg:text-primary sg:flex sg:items-center sg:justify-center sg:shrink-0\">\n <Bot className=\"sg:h-4 sg:w-4\" />\n </div>\n <div className=\"sg:bg-muted sg:rounded-2xl sg:rounded-tl-sm sg:px-4 sg:py-3 sg:flex sg:items-center sg:gap-1\">\n <span className=\"sg:w-2 sg:h-2 sg:rounded-full sg:bg-muted-foreground/40 sg:animate-bounce [animation-delay:0ms]\" />\n <span className=\"sg:w-2 sg:h-2 sg:rounded-full sg:bg-muted-foreground/40 sg:animate-bounce [animation-delay:150ms]\" />\n <span className=\"sg:w-2 sg:h-2 sg:rounded-full sg:bg-muted-foreground/40 sg:animate-bounce [animation-delay:300ms]\" />\n </div>\n </motion.div>\n )}\n </div>\n\n {/* Input area */}\n <div className=\"sg:pt-4 sg:mt-4 sg:border-t sg:border-border\">\n <div className=\"sg:flex sg:gap-2\">\n <Input\n ref={inputRef}\n placeholder=\"Type a message…\"\n value={input}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={handleKeyDown}\n disabled={isTyping}\n className=\"sg:flex-1\"\n />\n <Button\n onClick={sendMessage}\n disabled={!input.trim() || isTyping}\n aria-label=\"Send message\"\n >\n <Send className=\"sg:h-4 sg:w-4\" />\n </Button>\n </div>\n <Text className=\"sg:text-[11px] sg:text-muted-foreground sg:text-center sg:mt-2\">\n Demo mode — responses are pre-written. Try: hello, help, posts,\n design\n </Text>\n </div>\n </motion.div>\n </Layout.Col1>\n </Layout>\n );\n}\n\n/** Simple markdown-ish renderer for bold and line breaks */\nfunction MessageContent({ content }: { content: string }) {\n const lines = content.split(\"\\n\");\n return (\n <div className=\"sg:space-y-1.5\">\n {lines.map((line, i) => {\n if (line.trim() === \"\") return <br key={i} />;\n // Bold: **text**\n const parts = line.split(/(\\*\\*[^*]+\\*\\*)/g);\n return (\n <p key={i}>\n {parts.map((part, j) =>\n part.startsWith(\"**\") && part.endsWith(\"**\") ? (\n <strong key={j} className=\"sg:font-semibold\">\n {part.slice(2, -2)}\n </strong>\n ) : (\n <span key={j}>{part}</span>\n ),\n )}\n </p>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useForm } from "@tanstack/react-form";
|
|
4
4
|
import { motion } from "framer-motion";
|
|
5
|
+
import {
|
|
6
|
+
TanStackInputField,
|
|
7
|
+
TanStackTextareaField
|
|
8
|
+
} from "../../../lib/forms/tanstack-field.js";
|
|
5
9
|
import {
|
|
6
10
|
Layout,
|
|
7
11
|
Heading,
|
|
8
12
|
Text,
|
|
9
|
-
Input,
|
|
10
|
-
Textarea,
|
|
11
13
|
Button,
|
|
12
14
|
Icon
|
|
13
15
|
} from "../../primitives/index.js";
|
|
14
|
-
import {
|
|
16
|
+
import { FieldGroup } from "../../primitives/forms/field.js";
|
|
17
|
+
import {
|
|
18
|
+
Form,
|
|
19
|
+
FormActions
|
|
20
|
+
} from "../../primitives/forms/form.js";
|
|
15
21
|
import { PageHero } from "../../blocks/marketing/page-hero.js";
|
|
16
22
|
import { Card, CardContent } from "../../blocks/cards/card.js";
|
|
17
23
|
import { useToast } from "../../primitives/sonner/use-toast.js";
|
|
@@ -21,46 +27,22 @@ const initialForm = {
|
|
|
21
27
|
subject: "",
|
|
22
28
|
message: ""
|
|
23
29
|
};
|
|
24
|
-
function validateContact(form) {
|
|
25
|
-
const errors = {};
|
|
26
|
-
if (!form.name.trim()) errors.name = "Name is required";
|
|
27
|
-
if (!form.email.trim() || !form.email.includes("@")) {
|
|
28
|
-
errors.email = "Please enter a valid email";
|
|
29
|
-
}
|
|
30
|
-
if (!form.subject.trim()) errors.subject = "Subject is required";
|
|
31
|
-
if (!form.message.trim()) errors.message = "Message is required";
|
|
32
|
-
return errors;
|
|
33
|
-
}
|
|
34
30
|
function ContactPage() {
|
|
35
31
|
const { toast } = useToast();
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
setForm((prev) => ({ ...prev, [field]: value }));
|
|
41
|
-
if (errors[field]) setErrors((prev) => ({ ...prev, [field]: void 0 }));
|
|
42
|
-
};
|
|
43
|
-
const handleSubmit = (e) => {
|
|
44
|
-
e.preventDefault();
|
|
45
|
-
const fieldErrors = validateContact(form);
|
|
46
|
-
if (Object.keys(fieldErrors).length > 0) {
|
|
47
|
-
setErrors(fieldErrors);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
setSending(true);
|
|
51
|
-
setTimeout(() => {
|
|
52
|
-
setSending(false);
|
|
32
|
+
const form = useForm({
|
|
33
|
+
defaultValues: initialForm,
|
|
34
|
+
onSubmit: async ({ formApi }) => {
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
53
36
|
toast.message("Message sent!", {
|
|
54
37
|
description: "Thanks for reaching out. We'll get back to you soon."
|
|
55
38
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
39
|
+
formApi.reset();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
60
42
|
return /* @__PURE__ */ jsxs(
|
|
61
43
|
motion.div,
|
|
62
44
|
{
|
|
63
|
-
initial: { opacity:
|
|
45
|
+
initial: { opacity: 1, y: 20 },
|
|
64
46
|
animate: { opacity: 1, y: 0 },
|
|
65
47
|
transition: { duration: 0.3 },
|
|
66
48
|
children: [
|
|
@@ -75,69 +57,94 @@ function ContactPage() {
|
|
|
75
57
|
/* @__PURE__ */ jsx(Layout, { type: "col", className: "sg:py-16", children: /* @__PURE__ */ jsx(Layout.Col1, { hideDiv: true, className: "sg:max-w-5xl", children: /* @__PURE__ */ jsxs("div", { className: "sg:grid sg:gap-12 lg:sg:grid-cols-5", children: [
|
|
76
58
|
/* @__PURE__ */ jsxs("div", { className: "lg:sg:col-span-3", children: [
|
|
77
59
|
/* @__PURE__ */ jsx(Heading, { variant: "h3", className: "sg:mb-6", children: "Send a Message" }),
|
|
78
|
-
/* @__PURE__ */ jsxs(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
60
|
+
/* @__PURE__ */ jsxs(
|
|
61
|
+
Form,
|
|
62
|
+
{
|
|
63
|
+
noValidate: true,
|
|
64
|
+
onSubmit: (event) => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
void form.handleSubmit();
|
|
68
|
+
},
|
|
69
|
+
children: [
|
|
70
|
+
/* @__PURE__ */ jsxs(FieldGroup, { className: "sg:grid sg:gap-5 sm:sg:grid-cols-2", children: [
|
|
71
|
+
/* @__PURE__ */ jsx(
|
|
72
|
+
TanStackInputField,
|
|
73
|
+
{
|
|
74
|
+
formApi: form,
|
|
75
|
+
name: "name",
|
|
76
|
+
label: "Name",
|
|
77
|
+
placeholder: "Your name",
|
|
78
|
+
validators: {
|
|
79
|
+
onChange: ({ value }) => value.trim() ? void 0 : "Name is required"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
/* @__PURE__ */ jsx(
|
|
84
|
+
TanStackInputField,
|
|
85
|
+
{
|
|
86
|
+
formApi: form,
|
|
87
|
+
name: "email",
|
|
88
|
+
label: "Email",
|
|
89
|
+
type: "email",
|
|
90
|
+
placeholder: "you@example.com",
|
|
91
|
+
validators: {
|
|
92
|
+
onChange: ({ value }) => {
|
|
93
|
+
if (!value.trim() || !value.includes("@")) {
|
|
94
|
+
return "Please enter a valid email";
|
|
95
|
+
}
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] }),
|
|
102
|
+
/* @__PURE__ */ jsxs(FieldGroup, { className: "sg:gap-4", children: [
|
|
103
|
+
/* @__PURE__ */ jsx(
|
|
104
|
+
TanStackInputField,
|
|
105
|
+
{
|
|
106
|
+
formApi: form,
|
|
107
|
+
name: "subject",
|
|
108
|
+
label: "Subject",
|
|
109
|
+
placeholder: "What's this about?",
|
|
110
|
+
validators: {
|
|
111
|
+
onChange: ({ value }) => value.trim() ? void 0 : "Subject is required"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
/* @__PURE__ */ jsx(
|
|
116
|
+
TanStackTextareaField,
|
|
117
|
+
{
|
|
118
|
+
formApi: form,
|
|
119
|
+
name: "message",
|
|
120
|
+
label: "Message",
|
|
121
|
+
placeholder: "Your message...",
|
|
122
|
+
rows: 6,
|
|
123
|
+
validators: {
|
|
124
|
+
onChange: ({ value }) => value.trim() ? void 0 : "Message is required"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
] }),
|
|
129
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(
|
|
130
|
+
form.Subscribe,
|
|
98
131
|
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
132
|
+
selector: (state) => [state.canSubmit, state.isSubmitting],
|
|
133
|
+
children: ([canSubmit, isSubmitting]) => /* @__PURE__ */ jsx(
|
|
134
|
+
Button,
|
|
135
|
+
{
|
|
136
|
+
type: "submit",
|
|
137
|
+
iconStart: "Send",
|
|
138
|
+
disabled: !canSubmit || isSubmitting,
|
|
139
|
+
loading: isSubmitting,
|
|
140
|
+
children: "Send message"
|
|
141
|
+
}
|
|
142
|
+
)
|
|
105
143
|
}
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
/* @__PURE__ */ jsxs("div", { className: "sg:space-y-2", children: [
|
|
111
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: "contact-subject", children: "Subject" }),
|
|
112
|
-
/* @__PURE__ */ jsx(
|
|
113
|
-
Input,
|
|
114
|
-
{
|
|
115
|
-
id: "contact-subject",
|
|
116
|
-
value: form.subject,
|
|
117
|
-
onChange: (e) => handleChange("subject", e.target.value),
|
|
118
|
-
placeholder: "What's this about?",
|
|
119
|
-
"aria-invalid": !!errors.subject
|
|
120
|
-
}
|
|
121
|
-
),
|
|
122
|
-
errors.subject && /* @__PURE__ */ jsx(Text, { size: "sm", className: "sg:text-destructive", children: errors.subject })
|
|
123
|
-
] }),
|
|
124
|
-
/* @__PURE__ */ jsxs("div", { className: "sg:space-y-2", children: [
|
|
125
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: "contact-message", children: "Message" }),
|
|
126
|
-
/* @__PURE__ */ jsx(
|
|
127
|
-
Textarea,
|
|
128
|
-
{
|
|
129
|
-
id: "contact-message",
|
|
130
|
-
value: form.message,
|
|
131
|
-
onChange: (e) => handleChange("message", e.target.value),
|
|
132
|
-
placeholder: "Your message...",
|
|
133
|
-
rows: 6,
|
|
134
|
-
"aria-invalid": !!errors.message
|
|
135
|
-
}
|
|
136
|
-
),
|
|
137
|
-
errors.message && /* @__PURE__ */ jsx(Text, { size: "sm", className: "sg:text-destructive", children: errors.message })
|
|
138
|
-
] }),
|
|
139
|
-
/* @__PURE__ */ jsx(Button, { type: "submit", loading: sending, iconStart: "Send", children: "Send message" })
|
|
140
|
-
] })
|
|
144
|
+
) })
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
)
|
|
141
148
|
] }),
|
|
142
149
|
/* @__PURE__ */ jsx("div", { className: "lg:sg:col-span-2 sg:space-y-6", children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "sg:pt-6 sg:space-y-4", children: [
|
|
143
150
|
/* @__PURE__ */ jsxs("div", { className: "sg:flex sg:items-start sg:gap-3", children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/pages/contact/contact-page.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { motion } from \"framer-motion\";\r\nimport {\r\n\tLayout,\r\n\tHeading,\r\n\tText,\r\n\tInput,\r\n\tTextarea,\r\n\tButton,\r\n\tIcon,\r\n} from \"../../primitives/index\";\r\nimport { Label } from \"../../primitives/label/label\";\r\nimport { PageHero } from \"../../blocks/marketing/page-hero\";\r\nimport { Card, CardContent } from \"../../blocks/cards/card\";\r\nimport { useToast } from \"../../primitives/sonner/use-toast\";\r\n\r\ntype FormState = {\r\n\tname: string;\r\n\temail: string;\r\n\tsubject: string;\r\n\tmessage: string;\r\n};\r\n\r\nconst initialForm: FormState = {\r\n\tname: \"\",\r\n\temail: \"\",\r\n\tsubject: \"\",\r\n\tmessage: \"\",\r\n};\r\n\r\n/** Validates a simple contact form without external dependencies. */\r\nfunction validateContact(form: FormState): Partial<Record<keyof FormState, string>> {\r\n\tconst errors: Partial<Record<keyof FormState, string>> = {};\r\n\tif (!form.name.trim()) errors.name = \"Name is required\";\r\n\tif (!form.email.trim() || !form.email.includes(\"@\")) {\r\n\t\terrors.email = \"Please enter a valid email\";\r\n\t}\r\n\tif (!form.subject.trim()) errors.subject = \"Subject is required\";\r\n\tif (!form.message.trim()) errors.message = \"Message is required\";\r\n\treturn errors;\r\n}\r\n\r\nexport function ContactPage() {\r\n\tconst { toast } = useToast();\r\n\tconst [form, setForm] = useState<FormState>(initialForm);\r\n\tconst [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});\r\n\tconst [sending, setSending] = useState(false);\r\n\r\n\tconst handleChange = (field: keyof FormState, value: string) => {\r\n\t\tsetForm((prev) => ({ ...prev, [field]: value }));\r\n\t\tif (errors[field]) setErrors((prev) => ({ ...prev, [field]: undefined }));\r\n\t};\r\n\r\n\tconst handleSubmit = (e: React.FormEvent) => {\r\n\t\te.preventDefault();\r\n\t\tconst fieldErrors = validateContact(form);\r\n\t\tif (Object.keys(fieldErrors).length > 0) {\r\n\t\t\tsetErrors(fieldErrors);\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tsetSending(true);\r\n\t\tsetTimeout(() => {\r\n\t\t\tsetSending(false);\r\n\t\t\ttoast.message(\"Message sent!\", {\r\n\t\t\t\tdescription: \"Thanks for reaching out. We'll get back to you soon.\",\r\n\t\t\t});\r\n\t\t\tsetForm(initialForm);\r\n\t\t\tsetErrors({});\r\n\t\t}, 1200);\r\n\t};\r\n\r\n\treturn (\r\n\t\t<motion.div\r\n\t\t\tinitial={{ opacity: 0, y: 20 }}\r\n\t\t\tanimate={{ opacity: 1, y: 0 }}\r\n\t\t\ttransition={{ duration: 0.3 }}\r\n\t\t>\r\n\t\t\t<PageHero\r\n\t\t\t\ticon=\"Mail\"\r\n\t\t\t\ttitle=\"Get in Touch\"\r\n\t\t\t\tdescription=\"Have a question, idea, or just want to say hello? We'd love to hear from you.\"\r\n\t\t\t/>\r\n\r\n\t\t\t<Layout type=\"col\" className=\"sg:py-16\">\r\n\t\t\t\t<Layout.Col1 hideDiv className=\"sg:max-w-5xl\">\r\n\t\t\t\t\t<div className=\"sg:grid sg:gap-12 lg:sg:grid-cols-5\">\r\n\t\t\t\t\t\t<div className=\"lg:sg:col-span-3\">\r\n\t\t\t\t\t\t\t<Heading variant=\"h3\" className=\"sg:mb-6\">\r\n\t\t\t\t\t\t\t\tSend a Message\r\n\t\t\t\t\t\t\t</Heading>\r\n\t\t\t\t\t\t\t<form onSubmit={handleSubmit} className=\"sg:space-y-5\" noValidate>\r\n\t\t\t\t\t\t\t\t<div className=\"sg:grid sg:gap-5 sm:sg:grid-cols-2\">\r\n\t\t\t\t\t\t\t\t\t<div className=\"sg:space-y-2\">\r\n\t\t\t\t\t\t\t\t\t\t<Label htmlFor=\"contact-name\">Name</Label>\r\n\t\t\t\t\t\t\t\t\t\t<Input\r\n\t\t\t\t\t\t\t\t\t\t\tid=\"contact-name\"\r\n\t\t\t\t\t\t\t\t\t\t\tvalue={form.name}\r\n\t\t\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(\"name\", e.target.value)}\r\n\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"Your name\"\r\n\t\t\t\t\t\t\t\t\t\t\taria-invalid={!!errors.name}\r\n\t\t\t\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t\t\t\t\t{errors.name && (\r\n\t\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" className=\"sg:text-destructive\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t{errors.name}\r\n\t\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t\t)}\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div className=\"sg:space-y-2\">\r\n\t\t\t\t\t\t\t\t\t\t<Label htmlFor=\"contact-email\">Email</Label>\r\n\t\t\t\t\t\t\t\t\t\t<Input\r\n\t\t\t\t\t\t\t\t\t\t\tid=\"contact-email\"\r\n\t\t\t\t\t\t\t\t\t\t\ttype=\"email\"\r\n\t\t\t\t\t\t\t\t\t\t\tvalue={form.email}\r\n\t\t\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(\"email\", e.target.value)}\r\n\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"you@example.com\"\r\n\t\t\t\t\t\t\t\t\t\t\taria-invalid={!!errors.email}\r\n\t\t\t\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t\t\t\t\t{errors.email && (\r\n\t\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" className=\"sg:text-destructive\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t{errors.email}\r\n\t\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t\t)}\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t<div className=\"sg:space-y-2\">\r\n\t\t\t\t\t\t\t\t\t<Label htmlFor=\"contact-subject\">Subject</Label>\r\n\t\t\t\t\t\t\t\t\t<Input\r\n\t\t\t\t\t\t\t\t\t\tid=\"contact-subject\"\r\n\t\t\t\t\t\t\t\t\t\tvalue={form.subject}\r\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(\"subject\", e.target.value)}\r\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"What's this about?\"\r\n\t\t\t\t\t\t\t\t\t\taria-invalid={!!errors.subject}\r\n\t\t\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t\t\t\t{errors.subject && (\r\n\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" className=\"sg:text-destructive\">\r\n\t\t\t\t\t\t\t\t\t\t\t{errors.subject}\r\n\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t)}\r\n\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t<div className=\"sg:space-y-2\">\r\n\t\t\t\t\t\t\t\t\t<Label htmlFor=\"contact-message\">Message</Label>\r\n\t\t\t\t\t\t\t\t\t<Textarea\r\n\t\t\t\t\t\t\t\t\t\tid=\"contact-message\"\r\n\t\t\t\t\t\t\t\t\t\tvalue={form.message}\r\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(\"message\", e.target.value)}\r\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"Your message...\"\r\n\t\t\t\t\t\t\t\t\t\trows={6}\r\n\t\t\t\t\t\t\t\t\t\taria-invalid={!!errors.message}\r\n\t\t\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t\t\t\t{errors.message && (\r\n\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" className=\"sg:text-destructive\">\r\n\t\t\t\t\t\t\t\t\t\t\t{errors.message}\r\n\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t)}\r\n\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t<Button type=\"submit\" loading={sending} iconStart=\"Send\">\r\n\t\t\t\t\t\t\t\t\tSend message\r\n\t\t\t\t\t\t\t\t</Button>\r\n\t\t\t\t\t\t\t</form>\r\n\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t<div className=\"lg:sg:col-span-2 sg:space-y-6\">\r\n\t\t\t\t\t\t\t<Card>\r\n\t\t\t\t\t\t\t\t<CardContent className=\"sg:pt-6 sg:space-y-4\">\r\n\t\t\t\t\t\t\t\t\t<div className=\"sg:flex sg:items-start sg:gap-3\">\r\n\t\t\t\t\t\t\t\t\t\t<Icon icon=\"Mail\" className=\"sg:h-5 sg:w-5 sg:text-primary sg:mt-0.5\" />\r\n\t\t\t\t\t\t\t\t\t\t<div>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text fontweight=\"medium\">Email</Text>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" foreground=\"muted-foreground\">\r\n\t\t\t\t\t\t\t\t\t\t\t\thello@filion.se\r\n\t\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div className=\"sg:flex sg:items-start sg:gap-3\">\r\n\t\t\t\t\t\t\t\t\t\t<Icon icon=\"MapPin\" className=\"sg:h-5 sg:w-5 sg:text-primary sg:mt-0.5\" />\r\n\t\t\t\t\t\t\t\t\t\t<div>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text fontweight=\"medium\">Location</Text>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" foreground=\"muted-foreground\">\r\n\t\t\t\t\t\t\t\t\t\t\t\tStockholm, Sweden\r\n\t\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div className=\"sg:flex sg:items-start sg:gap-3\">\r\n\t\t\t\t\t\t\t\t\t\t<Icon icon=\"Clock\" className=\"sg:h-5 sg:w-5 sg:text-primary sg:mt-0.5\" />\r\n\t\t\t\t\t\t\t\t\t\t<div>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text fontweight=\"medium\">Hours</Text>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" foreground=\"muted-foreground\">\r\n\t\t\t\t\t\t\t\t\t\t\t\tMon–Fri, 9am–5pm CET\r\n\t\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t</CardContent>\r\n\t\t\t\t\t\t\t</Card>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</Layout.Col1>\r\n\t\t\t</Layout>\r\n\t\t</motion.div>\r\n\t);\r\n}\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/pages/contact/contact-page.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport { useForm } from \"@tanstack/react-form\";\r\nimport { motion } from \"framer-motion\";\r\nimport {\r\n\tTanStackInputField,\r\n\tTanStackTextareaField,\r\n} from \"../../../lib/forms/tanstack-field\";\r\nimport {\r\n\tLayout,\r\n\tHeading,\r\n\tText,\r\n\tButton,\r\n\tIcon,\r\n} from \"../../primitives/index\";\r\nimport { FieldGroup } from \"../../primitives/forms/field\";\r\nimport {\r\n\tForm,\r\n\tFormActions,\r\n} from \"../../primitives/forms/form\";\r\nimport { PageHero } from \"../../blocks/marketing/page-hero\";\r\nimport { Card, CardContent } from \"../../blocks/cards/card\";\r\nimport { useToast } from \"../../primitives/sonner/use-toast\";\r\n\r\ntype ContactFormValues = {\r\n\tname: string;\r\n\temail: string;\r\n\tsubject: string;\r\n\tmessage: string;\r\n};\r\n\r\nconst initialForm: ContactFormValues = {\r\n\tname: \"\",\r\n\temail: \"\",\r\n\tsubject: \"\",\r\n\tmessage: \"\",\r\n};\r\n\r\nexport function ContactPage() {\r\n\tconst { toast } = useToast();\r\n\r\n\tconst form = useForm({\r\n\t\tdefaultValues: initialForm,\r\n\t\tonSubmit: async ({ formApi }) => {\r\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 1200));\r\n\t\t\ttoast.message(\"Message sent!\", {\r\n\t\t\t\tdescription: \"Thanks for reaching out. We'll get back to you soon.\",\r\n\t\t\t});\r\n\t\t\tformApi.reset();\r\n\t\t},\r\n\t});\r\n\r\n\treturn (\r\n\t\t<motion.div\r\n\t\t\tinitial={{ opacity: 1, y: 20 }}\r\n\t\t\tanimate={{ opacity: 1, y: 0 }}\r\n\t\t\ttransition={{ duration: 0.3 }}\r\n\t\t>\r\n\t\t\t<PageHero\r\n\t\t\t\ticon=\"Mail\"\r\n\t\t\t\ttitle=\"Get in Touch\"\r\n\t\t\t\tdescription=\"Have a question, idea, or just want to say hello? We'd love to hear from you.\"\r\n\t\t\t/>\r\n\r\n\t\t\t<Layout type=\"col\" className=\"sg:py-16\">\r\n\t\t\t\t<Layout.Col1 hideDiv className=\"sg:max-w-5xl\">\r\n\t\t\t\t\t<div className=\"sg:grid sg:gap-12 lg:sg:grid-cols-5\">\r\n\t\t\t\t\t\t<div className=\"lg:sg:col-span-3\">\r\n\t\t\t\t\t\t\t<Heading variant=\"h3\" className=\"sg:mb-6\">\r\n\t\t\t\t\t\t\t\tSend a Message\r\n\t\t\t\t\t\t\t</Heading>\r\n\t\t\t\t\t\t\t<Form\r\n\t\t\t\t\t\t\t\tnoValidate\r\n\t\t\t\t\t\t\t\tonSubmit={(event) => {\r\n\t\t\t\t\t\t\t\t\tevent.preventDefault();\r\n\t\t\t\t\t\t\t\t\tevent.stopPropagation();\r\n\t\t\t\t\t\t\t\t\tvoid form.handleSubmit();\r\n\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t>\r\n\t\t\t\t\t\t\t\t<FieldGroup className=\"sg:grid sg:gap-5 sm:sg:grid-cols-2\">\r\n\t\t\t\t\t\t\t\t\t<TanStackInputField\r\n\t\t\t\t\t\t\t\t\t\tformApi={form}\r\n\t\t\t\t\t\t\t\t\t\tname=\"name\"\r\n\t\t\t\t\t\t\t\t\t\tlabel=\"Name\"\r\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"Your name\"\r\n\t\t\t\t\t\t\t\t\t\tvalidators={{\r\n\t\t\t\t\t\t\t\t\t\t\tonChange: ({ value }: { value: string }) =>\r\n\t\t\t\t\t\t\t\t\t\t\t\tvalue.trim() ? undefined : \"Name is required\",\r\n\t\t\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t\t\t\t<TanStackInputField\r\n\t\t\t\t\t\t\t\t\t\tformApi={form}\r\n\t\t\t\t\t\t\t\t\t\tname=\"email\"\r\n\t\t\t\t\t\t\t\t\t\tlabel=\"Email\"\r\n\t\t\t\t\t\t\t\t\t\ttype=\"email\"\r\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"you@example.com\"\r\n\t\t\t\t\t\t\t\t\t\tvalidators={{\r\n\t\t\t\t\t\t\t\t\t\t\tonChange: ({ value }: { value: string }) => {\r\n\t\t\t\t\t\t\t\t\t\t\t\tif (!value.trim() || !value.includes(\"@\")) {\r\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn \"Please enter a valid email\";\r\n\t\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t\treturn undefined;\r\n\t\t\t\t\t\t\t\t\t\t\t},\r\n\t\t\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t\t\t</FieldGroup>\r\n\t\t\t\t\t\t\t\t<FieldGroup className=\"sg:gap-4\">\r\n\t\t\t\t\t\t\t\t\t<TanStackInputField\r\n\t\t\t\t\t\t\t\t\t\tformApi={form}\r\n\t\t\t\t\t\t\t\t\t\tname=\"subject\"\r\n\t\t\t\t\t\t\t\t\t\tlabel=\"Subject\"\r\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"What's this about?\"\r\n\t\t\t\t\t\t\t\t\t\tvalidators={{\r\n\t\t\t\t\t\t\t\t\t\t\tonChange: ({ value }: { value: string }) =>\r\n\t\t\t\t\t\t\t\t\t\t\t\tvalue.trim() ? undefined : \"Subject is required\",\r\n\t\t\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t\t\t\t<TanStackTextareaField\r\n\t\t\t\t\t\t\t\t\t\tformApi={form}\r\n\t\t\t\t\t\t\t\t\t\tname=\"message\"\r\n\t\t\t\t\t\t\t\t\t\tlabel=\"Message\"\r\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"Your message...\"\r\n\t\t\t\t\t\t\t\t\t\trows={6}\r\n\t\t\t\t\t\t\t\t\t\tvalidators={{\r\n\t\t\t\t\t\t\t\t\t\t\tonChange: ({ value }: { value: string }) =>\r\n\t\t\t\t\t\t\t\t\t\t\t\tvalue.trim() ? undefined : \"Message is required\",\r\n\t\t\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t\t\t</FieldGroup>\r\n\t\t\t\t\t\t\t\t<FormActions>\r\n\t\t\t\t\t\t\t\t\t<form.Subscribe\r\n\t\t\t\t\t\t\t\t\t\tselector={(state) => [state.canSubmit, state.isSubmitting]}\r\n\t\t\t\t\t\t\t\t\t>\r\n\t\t\t\t\t\t\t\t\t\t{([canSubmit, isSubmitting]) => (\r\n\t\t\t\t\t\t\t\t\t\t\t<Button\r\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"submit\"\r\n\t\t\t\t\t\t\t\t\t\t\t\ticonStart=\"Send\"\r\n\t\t\t\t\t\t\t\t\t\t\t\tdisabled={!canSubmit || isSubmitting}\r\n\t\t\t\t\t\t\t\t\t\t\t\tloading={isSubmitting}\r\n\t\t\t\t\t\t\t\t\t\t\t>\r\n\t\t\t\t\t\t\t\t\t\t\t\tSend message\r\n\t\t\t\t\t\t\t\t\t\t\t</Button>\r\n\t\t\t\t\t\t\t\t\t\t)}\r\n\t\t\t\t\t\t\t\t\t</form.Subscribe>\r\n\t\t\t\t\t\t\t\t</FormActions>\r\n\t\t\t\t\t\t\t</Form>\r\n\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t<div className=\"lg:sg:col-span-2 sg:space-y-6\">\r\n\t\t\t\t\t\t\t<Card>\r\n\t\t\t\t\t\t\t\t<CardContent className=\"sg:pt-6 sg:space-y-4\">\r\n\t\t\t\t\t\t\t\t\t<div className=\"sg:flex sg:items-start sg:gap-3\">\r\n\t\t\t\t\t\t\t\t\t\t<Icon icon=\"Mail\" className=\"sg:h-5 sg:w-5 sg:text-primary sg:mt-0.5\" />\r\n\t\t\t\t\t\t\t\t\t\t<div>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text fontweight=\"medium\">Email</Text>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" foreground=\"muted-foreground\">\r\n\t\t\t\t\t\t\t\t\t\t\t\thello@filion.se\r\n\t\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div className=\"sg:flex sg:items-start sg:gap-3\">\r\n\t\t\t\t\t\t\t\t\t\t<Icon icon=\"MapPin\" className=\"sg:h-5 sg:w-5 sg:text-primary sg:mt-0.5\" />\r\n\t\t\t\t\t\t\t\t\t\t<div>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text fontweight=\"medium\">Location</Text>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" foreground=\"muted-foreground\">\r\n\t\t\t\t\t\t\t\t\t\t\t\tStockholm, Sweden\r\n\t\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div className=\"sg:flex sg:items-start sg:gap-3\">\r\n\t\t\t\t\t\t\t\t\t\t<Icon icon=\"Clock\" className=\"sg:h-5 sg:w-5 sg:text-primary sg:mt-0.5\" />\r\n\t\t\t\t\t\t\t\t\t\t<div>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text fontweight=\"medium\">Hours</Text>\r\n\t\t\t\t\t\t\t\t\t\t\t<Text size=\"sm\" foreground=\"muted-foreground\">\r\n\t\t\t\t\t\t\t\t\t\t\t\tMon–Fri, 9am–5pm CET\r\n\t\t\t\t\t\t\t\t\t\t\t</Text>\r\n\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t</CardContent>\r\n\t\t\t\t\t\t\t</Card>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</Layout.Col1>\r\n\t\t\t</Layout>\r\n\t\t</motion.div>\r\n\t);\r\n}\r\n"],"mappings":"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","names":[]}
|