-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor project structure, enhance UI components, and implement features #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: marvticle-monorepo
Are you sure you want to change the base?
Changes from all commits
b914d88
bf037d3
d8308fd
f9d1056
219238b
d92a879
1524e7b
530e390
2fd0a9c
8d4a115
af27f40
53d11bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { Avatar, AvatarFallback, AvatarImage } from '@trid/ui/components/avatar' | ||
| import { cn } from '@trid/ui/lib/utils' | ||
|
|
||
| import { GeneratedAvatar } from '#/components/generated-avatar' | ||
|
|
||
| export const AvatarUser = ({ | ||
| image, | ||
| name, | ||
| className, | ||
| }: { | ||
| image?: string | null | ||
| name: string | ||
| className?: string | ||
| }) => { | ||
| if (!image) { | ||
| return ( | ||
| <GeneratedAvatar | ||
| seed={name} | ||
| style="notionistsNeutral" | ||
| className={cn('size-8 rounded-full after:border-none', className)} | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <Avatar className={cn('size-8 rounded-full after:border-none', className)}> | ||
| <AvatarImage src={image} alt={name} className="rounded-full" /> | ||
| <AvatarFallback | ||
| className={cn('size-8 rounded-full uppercase', className)} | ||
| > | ||
| {name.charAt(0)} | ||
| </AvatarFallback> | ||
| </Avatar> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import { useQueryErrorResetBoundary } from '@tanstack/react-query' | ||
| import { Link, useRouter } from '@tanstack/react-router' | ||
|
|
||
| import { useEffect } from 'react' | ||
|
|
||
| import { | ||
| AlertTriangleIcon, | ||
| ArrowUpRightFromSquareIcon, | ||
| HomeIcon, | ||
| RefreshCcwIcon, | ||
| } from 'lucide-react' | ||
|
|
||
| import { Button } from '@trid/ui/components/button' | ||
| import { | ||
| Dialog, | ||
| DialogClose, | ||
| DialogContent, | ||
| DialogDescription, | ||
| DialogFooter, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| DialogTrigger, | ||
| } from '@trid/ui/components/dialog' | ||
| import { | ||
| Empty, | ||
| EmptyContent, | ||
| EmptyDescription, | ||
| EmptyHeader, | ||
| EmptyMedia, | ||
| EmptyTitle, | ||
| } from '@trid/ui/components/empty' | ||
|
|
||
| export const DefaultErrorComponent = ({ error }: { error: Error }) => { | ||
| const isDev = process.env.NODE_ENV !== 'production' | ||
|
|
||
| const router = useRouter() | ||
| const queryClientErrorBoundary = useQueryErrorResetBoundary() | ||
|
|
||
| const handleRetry = () => { | ||
| void router.invalidate() | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| queryClientErrorBoundary.reset() | ||
| }, [queryClientErrorBoundary]) | ||
|
|
||
| return ( | ||
| <div className="relative flex h-svh w-full items-center justify-center overflow-hidden"> | ||
| <Empty> | ||
| <EmptyHeader> | ||
| <EmptyMedia | ||
| variant="icon" | ||
| className="bg-destructive/30 text-destructive ring ring-destructive/50" | ||
| > | ||
| <AlertTriangleIcon /> | ||
| </EmptyMedia> | ||
| <EmptyTitle>Oops! Something went wrong</EmptyTitle> | ||
| <EmptyDescription> | ||
| It looks like something unexpected happened. | ||
| <br /> | ||
| Please try again later. | ||
| </EmptyDescription> | ||
| </EmptyHeader> | ||
| <EmptyContent className="flex-row justify-center gap-2"> | ||
| <Button type="button" asChild> | ||
| <Link to="/"> | ||
| <HomeIcon /> | ||
| Go to Home | ||
| </Link> | ||
| </Button> | ||
|
|
||
| <Button type="button" variant="outline" onClick={handleRetry}> | ||
| <RefreshCcwIcon /> | ||
| Try again | ||
| </Button> | ||
| </EmptyContent> | ||
|
|
||
| {isDev && ( | ||
| <Dialog> | ||
| <DialogTrigger asChild> | ||
| <Button | ||
| type="button" | ||
| variant="link" | ||
| className="text-muted-foreground" | ||
| > | ||
| Learn More <ArrowUpRightFromSquareIcon /> | ||
| </Button> | ||
| </DialogTrigger> | ||
|
|
||
| <DialogContent> | ||
| <DialogHeader> | ||
| <DialogTitle>Learn More</DialogTitle> | ||
| <DialogDescription>Error details</DialogDescription> | ||
| </DialogHeader> | ||
|
|
||
| <div className="max-h-96 w-full overflow-auto rounded-md bg-muted p-4"> | ||
| <h3 className="mb-2 font-semibold">Error Message:</h3> | ||
| <p className="mb-4 text-sm">{error.message}</p> | ||
| <h3 className="mb-2 font-semibold">Stack Trace:</h3> | ||
| <pre className="text-xs break-all whitespace-pre-wrap"> | ||
| {error.stack} | ||
| </pre> | ||
| </div> | ||
|
|
||
| <DialogFooter> | ||
| <DialogClose asChild> | ||
| <Button type="button" variant="outline" className="w-full"> | ||
| Close | ||
| </Button> | ||
| </DialogClose> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> | ||
| )} | ||
| </Empty> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { useMemo } from 'react' | ||
|
|
||
| import { | ||
| botttsNeutral, | ||
| initials, | ||
| notionistsNeutral, | ||
| } from '@dicebear/collection' | ||
| import { createAvatar } from '@dicebear/core' | ||
|
|
||
| import { Avatar, AvatarFallback, AvatarImage } from '@trid/ui/components/avatar' | ||
| import { cn } from '@trid/ui/lib/utils' | ||
|
|
||
| interface Props { | ||
| seed: string | ||
| className?: string | ||
| style: 'botttsNeutral' | 'initials' | 'notionistsNeutral' | ||
| } | ||
|
|
||
| export const GeneratedAvatar = ({ seed, style, className }: Props) => { | ||
| const avatar = useMemo(() => { | ||
| const avatarVariants = { | ||
| botttsNeutral: () => createAvatar(botttsNeutral, { seed }), | ||
| initials: () => createAvatar(initials, { seed }), | ||
| notionistsNeutral: () => createAvatar(notionistsNeutral, { seed }), | ||
| } | ||
|
|
||
| return avatarVariants[style]() | ||
| }, [seed, style]) | ||
|
|
||
| const avatarUri = useMemo(() => avatar.toDataUri(), [avatar]) | ||
|
|
||
| return ( | ||
| <Avatar className={cn(className)}> | ||
| <AvatarImage src={avatarUri} alt={seed} className={cn(className)} /> | ||
| <AvatarFallback className={cn('uppercase', className)}> | ||
| {Array.from(seed)[0] ?? '?'} | ||
| </AvatarFallback> | ||
| </Avatar> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { motion } from 'motion/react' | ||
|
|
||
| import { cn } from '@trid/ui/lib/utils' | ||
|
|
||
| function InlineLoader({ className }: { className?: string }) { | ||
| return ( | ||
| <motion.span | ||
| className={cn('inline-flex items-center gap-3', className)} | ||
| initial={{ opacity: 0 }} | ||
| animate={{ opacity: 1 }} | ||
| exit={{ opacity: 0 }} | ||
| transition={{ duration: 0.15 }} | ||
| > | ||
| {[0, 0.15, 0.3].map((delay, i) => ( | ||
| <motion.span | ||
| key={i} | ||
| className="inline-block size-3 rounded-full bg-foreground/60" | ||
| animate={{ opacity: [0.4, 1, 0.4], y: [0, -3, 0] }} | ||
| transition={{ repeat: Infinity, duration: 0.8, delay }} | ||
| /> | ||
| ))} | ||
| </motion.span> | ||
| ) | ||
| } | ||
|
|
||
| export const Loader = { | ||
| Inline: InlineLoader, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import type { SVGProps } from 'react' | ||
|
|
||
| type MarvIconProps = SVGProps<SVGSVGElement> | ||
|
|
||
| export const MarvIcon = ({ ...props }: MarvIconProps) => { | ||
| return ( | ||
| <svg | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| version="1.1" | ||
| viewBox="0 0 460.11 409.97" | ||
| {...props} | ||
| > | ||
| <polygon | ||
| points="55.81 0 171.96 292.62 285.86 0 338.25 0 218.7 301.18 221.52 301.74 287.57 235.69 342.81 235.69 171.42 409.97 0 235.69 55.24 235.69 122.44 302.87 122.72 299.17 3.42 0 55.81 0" | ||
| fill="currentColor" | ||
| /> | ||
| <rect | ||
| x="369" | ||
| y="278.96" | ||
| width="91.11" | ||
| height="93.37" | ||
| fill="currentColor" | ||
| /> | ||
| </svg> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { PanelLeftCloseIcon, PanelLeftIcon } from 'lucide-react' | ||
|
|
||
| import { Button } from '@trid/ui/components/button' | ||
| import { useSidebar } from '@trid/ui/components/sidebar' | ||
|
|
||
| export const Navbar = () => { | ||
| const { state, isMobile, toggleSidebar } = useSidebar() | ||
|
|
||
| return ( | ||
| <header className="sticky top-0 z-50 h-12 w-full rounded-t-2xl bg-background/90 backdrop-blur supports-backdrop-filter:bg-background/60 max-md:rounded-t-none"> | ||
| <div className="flex h-full items-center justify-between px-4 py-2"> | ||
| <Button | ||
| type="button" | ||
| size="icon" | ||
| variant="ghost" | ||
| onClick={toggleSidebar} | ||
| aria-label={ | ||
| state === 'collapsed' || isMobile | ||
| ? 'Expand sidebar' | ||
| : 'Collapse sidebar' | ||
| } | ||
| > | ||
| {state === 'collapsed' || isMobile ? ( | ||
| <PanelLeftIcon /> | ||
| ) : ( | ||
| <PanelLeftCloseIcon /> | ||
| )} | ||
|
Comment on lines
+17
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify useSidebar exposes openMobile/state so the toggle can reflect the actual open state on mobile.
ast-grep --pattern 'const useSidebar = () => { $$$ }'
rg -nP '\b(openMobile|isMobile|state)\b' packages/ui/src/components/sidebar.tsx -C2Repository: isntboxs/marvticle Length of output: 3623 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the navbar toggle code around the referenced lines
echo "---- navbar.tsx (lines 1-80) ----"
nl -ba apps/web/src/components/navbar.tsx | sed -n '1,120p'
# Find where openMobile is referenced/consumed in navbar.tsx
echo "---- search openMobile usage in navbar.tsx ----"
rg -n "openMobile|setOpenMobile" apps/web/src/components/navbar.tsx || true
# Locate how navbar imports/uses the sidebar hook/context
echo "---- search for useSidebar usage in navbar.tsx ----"
rg -n "useSidebar" apps/web/src/components/navbar.tsx
# Inspect sidebar hook return shape for what navbar might be able to use
echo "---- sidebar implementation snippet (useSidebar context) ----"
rg -n "openMobile|isMobile|toggleSidebar" packages/ui/src/components/sidebar.tsx -nRepository: isntboxs/marvticle Length of output: 157 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Show navbar.tsx with line numbers (using awk instead of nl)
echo "---- navbar.tsx (first 160 lines) ----"
awk '{printf "%5d\t%s\n", NR, $0}' apps/web/src/components/navbar.tsx | sed -n '1,160p'
echo "---- search openMobile usage in navbar.tsx ----"
rg -n "openMobile|setOpenMobile" apps/web/src/components/navbar.tsx || true
echo "---- search useSidebar usage in navbar.tsx ----"
rg -n "useSidebar" apps/web/src/components/navbar.tsx || true
echo "---- show relevant snippet around the toggle (look for Expand/Collapse sidebar strings) ----"
rg -n "Expand sidebar|Collapse sidebar" apps/web/src/components/navbar.tsx -n -C3 || trueRepository: isntboxs/marvticle Length of output: 1710 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Show the relevant parts of sidebar.tsx: context shape, state/openMobile derivation, and isMobile computation
echo "---- sidebar.tsx: SidebarContextProps + hook return (first 220 lines) ----"
awk '{printf "%5d\t%s\n", NR, $0}' packages/ui/src/components/sidebar.tsx | sed -n '1,240p'
echo "---- sidebar.tsx: useIsMobile + toggleSidebar + state/openMobile (around toggle/state) ----"
rg -n "useIsMobile|toggleSidebar|openMobile|const state" packages/ui/src/components/sidebar.tsx -n
echo "---- sidebar.tsx: exact lines around openMobile & state (lines 55-140) ----"
awk '{printf "%5d\t%s\n", NR, $0}' packages/ui/src/components/sidebar.tsx | sed -n '50,160p'
echo "---- sidebar.tsx: mobile rendering uses openMobile (around isMobile block) ----"
awk '{printf "%5d\t%s\n", NR, $0}' packages/ui/src/components/sidebar.tsx | sed -n '160,230p'Repository: isntboxs/marvticle Length of output: 15306 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n "function useIsMobile|export const useIsMobile|export function useIsMobile" -S packages/ui/src/hooks/use-mobile* packages/ui/src/hooks -g'*.ts' -g'*.tsx'Repository: isntboxs/marvticle Length of output: 202 🏁 Script executed: #!/bin/bash
set -euo pipefail
awk '{printf "%5d\t%s\n", NR, $0}' packages/ui/src/hooks/use-mobile.ts | sed -n '1,120p'Repository: isntboxs/marvticle Length of output: 722 Fix toggle label/icon to reflect mobile sidebar open state. On mobile, Update 🤖 Prompt for AI Agents |
||
| </Button> | ||
| </div> | ||
| </header> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { Link } from '@tanstack/react-router' | ||
|
|
||
| import { HomeIcon } from 'lucide-react' | ||
|
|
||
| import { Button } from '@trid/ui/components/button' | ||
| import { | ||
| Empty, | ||
| EmptyContent, | ||
| EmptyDescription, | ||
| EmptyHeader, | ||
| EmptyTitle, | ||
| } from '@trid/ui/components/empty' | ||
|
|
||
| export const DefaultNotFoundComponent = () => { | ||
| return ( | ||
| <div className="relative flex h-svh w-full items-center justify-center overflow-hidden"> | ||
| <Empty> | ||
| <EmptyHeader> | ||
| <EmptyTitle className="mask-b-from-20% mask-b-to-80% text-9xl font-extrabold"> | ||
| 404 | ||
| </EmptyTitle> | ||
| <EmptyDescription className="-mt-4 text-nowrap text-foreground/80"> | ||
| The page you're looking for might have been <br /> | ||
| moved or doesn't exist. | ||
| </EmptyDescription> | ||
| </EmptyHeader> | ||
| <EmptyContent> | ||
| <div className="flex gap-2"> | ||
| <Button asChild> | ||
| <Link to="/" viewTransition> | ||
| <HomeIcon data-icon="inline-start" /> | ||
| Go Home | ||
| </Link> | ||
| </Button> | ||
| </div> | ||
| </EmptyContent> | ||
| </Empty> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { Loader } from '#/components/loader' | ||
|
|
||
| export const DefaultPendingComponent = () => { | ||
| return ( | ||
| <div className="flex h-svh w-full items-center justify-center"> | ||
| <Loader.Inline /> | ||
| </div> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review the useEffect dependency array.
The
useEffectincludesqueryClientErrorBoundaryin the dependency array. IfuseQueryErrorResetBoundary()returns a new object reference on each render, this effect will run repeatedly instead of only once on mount. Typically, the error boundary reset should only run once when the error component mounts.♻️ Suggested fix
useEffect(() => { queryClientErrorBoundary.reset() -}, [queryClientErrorBoundary]) + // eslint-disable-next-line react-hooks/exhaustive-deps +}, [])📝 Committable suggestion
🤖 Prompt for AI Agents