Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@
"test": "vitest run"
},
"dependencies": {
"@bprogress/react": "catalog:",
"@dicebear/collection": "catalog:",
"@dicebear/core": "catalog:",
"@orpc/client": "catalog:",
"@orpc/contract": "catalog:",
"@orpc/json-schema": "catalog:",
"@orpc/openapi": "catalog:",
"@orpc/server": "catalog:",
"@orpc/tanstack-query": "catalog:",
"@orpc/zod": "catalog:",
"@tabler/icons-react": "catalog:",
"@tailwindcss/typography": "catalog:",
"@tailwindcss/vite": "catalog:",
"@tanstack/react-devtools": "catalog:",
Expand All @@ -36,10 +40,16 @@
"@trid/shared": "workspace:*",
"@trid/ui": "workspace:*",
"better-auth": "catalog:",
"date-fns": "catalog:",
"lucide-react": "catalog:",
"nitro": "npm:nitro-nightly@latest",
"motion": "catalog:",
"next-themes": "catalog:",
"nitro": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-icons": "catalog:",
"react-intersection-observer": "catalog:",
"sonner": "catalog:",
"tailwindcss": "catalog:",
"zod": "catalog:"
},
Expand Down
35 changes: 35 additions & 0 deletions apps/web/src/components/avatar-user.tsx
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>
)
}
118 changes: 118 additions & 0 deletions apps/web/src/components/error-components.tsx
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])
Comment on lines +43 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Review the useEffect dependency array.

The useEffect includes queryClientErrorBoundary in the dependency array. If useQueryErrorResetBoundary() 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
queryClientErrorBoundary.reset()
}, [queryClientErrorBoundary])
useEffect(() => {
queryClientErrorBoundary.reset()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/error-components.tsx` around lines 43 - 45, The
effect currently depends on queryClientErrorBoundary which may be a new object
each render; change the useEffect in the error component so
queryClientErrorBoundary.reset() runs only on mount (replace the dependency
array [queryClientErrorBoundary] with []), or if ESLint complains, keep the
empty array and add a concise comment/disable for the specific exhaustive-deps
rule; reference the useEffect that calls queryClientErrorBoundary.reset and the
hook useQueryErrorResetBoundary() that provides 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>
)
}
40 changes: 40 additions & 0 deletions apps/web/src/components/generated-avatar.tsx
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>
)
}
28 changes: 28 additions & 0 deletions apps/web/src/components/loader.tsx
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,
}
26 changes: 26 additions & 0 deletions apps/web/src/components/marv-icon.tsx
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>
)
}
32 changes: 32 additions & 0 deletions apps/web/src/components/navbar.tsx
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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 -C2

Repository: 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 -n

Repository: 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 || true

Repository: 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, isMobile is driven by viewport width, so Navbar’s condition (state === 'collapsed' || isMobile ? 'Expand sidebar' : 'Collapse sidebar' and the icon) always picks the “Expand” variant even when the mobile Sheet is open. toggleSidebar toggles openMobile, but Navbar only reads state and isMobile.

Update Navbar to also use openMobile and switch the aria-label/icon based on openMobile when isMobile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/navbar.tsx` around lines 17 - 27, Navbar's aria-label
and icon logic uses only state and isMobile, causing the mobile button to always
show the "Expand" variant; update the condition to check openMobile when
isMobile. Modify the JSX where aria-label and icon are chosen (the conditional
using state === 'collapsed' || isMobile) to instead use (isMobile ? openMobile :
state !== 'collapsed') or equivalent so that when isMobile the label/icon
reflect openMobile; ensure you reference the openMobile boolean from the same
props/state used by toggleSidebar and update both the aria-label string and the
icon selection (PanelLeftIcon vs PanelLeftCloseIcon) accordingly.

</Button>
</div>
</header>
)
}
40 changes: 40 additions & 0 deletions apps/web/src/components/not-found-components.tsx
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>
)
}
9 changes: 9 additions & 0 deletions apps/web/src/components/pending-components.tsx
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>
)
}
Loading