format code + change env

This commit is contained in:
Thomas Camlong
2025-10-01 19:01:31 +02:00
parent 0a4a4a78f4
commit 49aab75953
19 changed files with 1282 additions and 1468 deletions

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"@tanstack/react-table": "^8.21.3"
}
}

57
pnpm-lock.yaml generated
View File

@@ -1,57 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
packages:
'@tanstack/react-table@8.21.3':
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
engines: {node: '>=12'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/table-core@8.21.3':
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
react-dom@19.1.1:
resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
peerDependencies:
react: ^19.1.1
react@19.1.1:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
engines: {node: '>=0.10.0'}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
snapshots:
'@tanstack/react-table@8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@tanstack/table-core': 8.21.3
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
'@tanstack/table-core@8.21.3': {}
react-dom@19.1.1(react@19.1.1):
dependencies:
react: 19.1.1
scheduler: 0.26.0
react@19.1.1: {}
scheduler@0.26.0: {}

View File

@@ -34,4 +34,3 @@ export async function revalidateAllSubmissions() {
return { success: false, error: "Failed to revalidate" }
}
}

View File

@@ -53,4 +53,3 @@ export default async function CommunityPage() {
</div>
)
}

View File

@@ -1,24 +1,19 @@
"use client"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { SubmissionsDataTable } from "@/components/submissions-data-table"
import { useAuth, useSubmissions, useApproveSubmission, useRejectSubmission } from "@/hooks/use-submissions"
import { Skeleton } from "@/components/ui/skeleton"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { AlertCircle, RefreshCw } from "lucide-react"
import { SubmissionsDataTable } from "@/components/submissions-data-table"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import { useApproveSubmission, useAuth, useRejectSubmission, useSubmissions } from "@/hooks/use-submissions"
export default function DashboardPage() {
// Fetch auth status
const { data: auth, isLoading: authLoading } = useAuth()
// Fetch submissions
const {
data: submissions = [],
isLoading: submissionsLoading,
error: submissionsError,
refetch
} = useSubmissions()
const { data: submissions = [], isLoading: submissionsLoading, error: submissionsError, refetch } = useSubmissions()
// Mutations
const approveMutation = useApproveSubmission()
@@ -27,7 +22,7 @@ export default function DashboardPage() {
const isLoading = authLoading || submissionsLoading
const isAuthenticated = auth?.isAuthenticated ?? false
const isAdmin = auth?.isAdmin ?? false
const currentUserId = auth?.userId ?? ''
const currentUserId = auth?.userId ?? ""
const handleApprove = (submissionId: string) => {
approveMutation.mutate(submissionId)
@@ -85,10 +80,7 @@ export default function DashboardPage() {
<CardHeader>
<CardTitle>Submissions Dashboard</CardTitle>
<CardDescription>
{isAdmin
? "Review and manage all icon submissions."
: "View your icon submissions and track their status."
}
{isAdmin ? "Review and manage all icon submissions." : "View your icon submissions and track their status."}
</CardDescription>
</CardHeader>
<CardContent>
@@ -97,12 +89,7 @@ export default function DashboardPage() {
<AlertTitle>Error loading submissions</AlertTitle>
<AlertDescription>
Failed to load submissions. Please try again.
<Button
variant="outline"
size="sm"
className="ml-4"
onClick={() => refetch()}
>
<Button variant="outline" size="sm" className="ml-4" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4 mr-2" />
Retry
</Button>
@@ -123,8 +110,7 @@ export default function DashboardPage() {
<CardDescription>
{isAdmin
? "Review and manage all icon submissions. Click on a row to see details."
: "View your icon submissions and track their status."
}
: "View your icon submissions and track their status."}
</CardDescription>
</CardHeader>
<CardContent>

View File

@@ -8,8 +8,8 @@ import { PostHogProvider } from "@/components/PostHogProvider"
import { BASE_URL, getDescription, WEB_URL, websiteTitle } from "@/constants"
import { getTotalIcons } from "@/lib/api"
import "./globals.css"
import { ThemeProvider } from "./theme-provider"
import { Providers } from "@/components/providers"
import { ThemeProvider } from "./theme-provider"
const inter = Inter({
variable: "--font-inter",

View File

@@ -126,7 +126,7 @@ export function CommunityIconSearch({ icons }: CommunityIconSearchProps) {
for (const icon of filteredIcons) {
const iconWithStatus = icon as IconWithStatus
const status = iconWithStatus.status || 'pending'
const status = iconWithStatus.status || "pending"
if (!groups[status]) {
groups[status] = []
@@ -136,8 +136,7 @@ export function CommunityIconSearch({ icons }: CommunityIconSearchProps) {
return Object.entries(groups)
.sort(([a], [b]) => {
return (statusPriority[a as keyof typeof statusPriority] ?? 999) -
(statusPriority[b as keyof typeof statusPriority] ?? 999)
return (statusPriority[a as keyof typeof statusPriority] ?? 999) - (statusPriority[b as keyof typeof statusPriority] ?? 999)
})
.map(([status, items]) => ({ status, items }))
}, [filteredIcons])
@@ -410,7 +409,7 @@ export function CommunityIconSearch({ icons }: CommunityIconSearchProps) {
{getStatusDisplayName(status)}
</Badge>
<span className="text-sm text-muted-foreground">
{items.length} {items.length === 1 ? 'icon' : 'icons'}
{items.length} {items.length === 1 ? "icon" : "icons"}
</span>
</div>
<Card className="bg-background/50 border shadow-lg">
@@ -425,4 +424,3 @@ export function CommunityIconSearch({ icons }: CommunityIconSearchProps) {
</>
)
}

View File

@@ -1,6 +1,6 @@
"use client"
import { Github, LogOut, PlusCircle, Search, Star, LayoutDashboard } from "lucide-react"
import { Github, LayoutDashboard, LogOut, PlusCircle, Search, Star } from "lucide-react"
import Link from "next/link"
import { useEffect, useState } from "react"
import { IconSubmissionForm } from "@/components/icon-submission-form"
@@ -14,12 +14,7 @@ import { CommandMenu } from "./command-menu"
import { HeaderNav } from "./header-nav"
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"
import { Button } from "./ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./ui/dropdown-menu"
import { DropdownMenu, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuTrigger } from "./ui/dropdown-menu"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"
interface UserData {
@@ -119,13 +114,8 @@ export function Header() {
<header className="border-b sticky top-0 z-50 backdrop-blur-2xl bg-background/50 border-border/50">
<div className="px-4 md:px-12 flex items-center justify-between h-16 md:h-18">
<div className="flex items-center gap-2 md:gap-6">
<Link
href="/"
className="text-lg md:text-xl font-bold group hidden md:block"
>
<span className="transition-colors duration-300 group-hover:">
Dashboard Icons
</span>
<Link href="/" className="text-lg md:text-xl font-bold group hidden md:block">
<span className="transition-colors duration-300 group-hover:">Dashboard Icons</span>
</Link>
<div className="flex-nowrap">
<HeaderNav isLoggedIn={isLoggedIn} />
@@ -134,11 +124,7 @@ export function Header() {
<div className="flex items-center gap-2 md:gap-4">
{/* Desktop search button */}
<div className="hidden md:block">
<Button
variant="outline"
className="gap-2 cursor-pointer transition-all duration-300"
onClick={openCommandMenu}
>
<Button variant="outline" className="gap-2 cursor-pointer transition-all duration-300" onClick={openCommandMenu}>
<Search className="h-4 w-4 transition-all duration-300" />
<span>Find icons</span>
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border border-border/80 bg-muted/80 px-1.5 font-mono text-[10px] font-medium opacity-100">
@@ -165,11 +151,7 @@ export function Header() {
{isLoggedIn ? (
<IconSubmissionForm
trigger={
<Button
variant="ghost"
size="icon"
className="rounded-lg cursor-pointer transition-all duration-300 hover:ring-2"
>
<Button variant="ghost" size="icon" className="rounded-lg cursor-pointer transition-all duration-300 hover:ring-2">
<PlusCircle className="h-5 w-5 transition-all duration-300" />
<span className="sr-only">Submit icon(s)</span>
</Button>
@@ -201,11 +183,7 @@ export function Header() {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className="rounded-lg cursor-pointer transition-all duration-300 hover:ring-2 gap-1.5"
asChild
>
<Button variant="ghost" className="rounded-lg cursor-pointer transition-all duration-300 hover:ring-2 gap-1.5" asChild>
<Link href={REPO_PATH} target="_blank" className="group flex items-center">
<Github className="h-5 w-5 group-hover: transition-all duration-300" />
{stars > 0 && (
@@ -234,13 +212,8 @@ export function Header() {
size="icon"
>
<Avatar className="h-8 w-8">
<AvatarImage
src={userData.avatar || "/placeholder.svg"}
alt={userData.username}
/>
<AvatarFallback className="text-xs">
{userData.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
<AvatarImage src={userData.avatar || "/placeholder.svg"} alt={userData.username} />
<AvatarFallback className="text-xs">{userData.username.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<span className="sr-only">User menu</span>
</Button>
@@ -249,29 +222,18 @@ export function Header() {
<div className="space-y-3">
<div className="flex items-center gap-3 px-1">
<Avatar className="h-10 w-10">
<AvatarImage
src={userData.avatar || "/placeholder.svg"}
alt={userData.username}
/>
<AvatarFallback className="text-sm font-semibold">
{userData.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
<AvatarImage src={userData.avatar || "/placeholder.svg"} alt={userData.username} />
<AvatarFallback className="text-sm font-semibold">{userData.username.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
<p className="text-sm font-semibold truncate">{userData.username}</p>
<p className="text-xs text-muted-foreground truncate">
{userData.email}
</p>
<p className="text-xs text-muted-foreground truncate">{userData.email}</p>
</div>
</div>
<DropdownMenuSeparator />
<Button
asChild
variant="ghost"
className="w-full justify-start gap-2 hover:bg-muted"
>
<Button asChild variant="ghost" className="w-full justify-start gap-2 hover:bg-muted">
<Link href="/dashboard">
<LayoutDashboard className="h-4 w-4" />
Dashboard
@@ -294,13 +256,7 @@ export function Header() {
</div>
{/* Single instance of CommandMenu */}
{isLoaded && (
<CommandMenu
icons={iconsData}
open={commandMenuOpen}
onOpenChange={setCommandMenuOpen}
/>
)}
{isLoaded && <CommandMenu icons={iconsData} open={commandMenuOpen} onOpenChange={setCommandMenuOpen} />}
{/* Login Modal */}
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />

View File

@@ -9,9 +9,7 @@ export function IconCard({ name, data: iconData, matchedAlias }: { name: string;
const formatedIconName = formatIconName(name)
const isCommunityIcon = iconData.base.startsWith("http")
const imageUrl = isCommunityIcon
? iconData.base
: `${BASE_URL}/${iconData.base}/${iconData.colors?.light || name}.${iconData.base}`
const imageUrl = isCommunityIcon ? iconData.base : `${BASE_URL}/${iconData.base}/${iconData.colors?.light || name}.${iconData.base}`
const linkHref = isCommunityIcon ? `/community/${name}` : `/icons/${name}`

View File

@@ -4,13 +4,7 @@ import { Github } from "lucide-react"
import type React from "react"
import { useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
@@ -30,7 +24,6 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
const [error, setError] = useState("")
const [isLoading, setIsLoading] = useState(false)
const emailRef = useRef<HTMLInputElement>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
@@ -95,13 +88,9 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-background border shadow-2xl ">
<DialogHeader className="space-y-3">
<DialogTitle className="text-2xl font-bold">
{isRegister ? "Create account" : "Sign in"}
</DialogTitle>
<DialogTitle className="text-2xl font-bold">{isRegister ? "Create account" : "Sign in"}</DialogTitle>
<DialogDescription className="text-base">
{isRegister
? "Enter your details to create an account"
: "Enter your credentials to continue"}
{isRegister ? "Enter your details to create an account" : "Enter your credentials to continue"}
</DialogDescription>
</DialogHeader>
@@ -109,7 +98,11 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
{error && (
<div className="text-sm text-destructive bg-destructive/10 border border-destructive/20 px-4 py-3 rounded-lg flex items-start gap-2">
<svg className="h-5 w-5 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
<span>{error}</span>
</div>
@@ -135,7 +128,7 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
<fieldset className="space-y-2 border-0 p-0">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium">
Email or Username
Email {!isRegister && "or Username"}
</Label>
<Input
id="email"
@@ -145,7 +138,7 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
name="email"
type="text"
autoComplete="username"
placeholder="Enter your email or username"
placeholder={`Enter your email${isRegister ? "" : " or username"}`}
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={error ? "true" : "false"}
@@ -153,9 +146,7 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
required
/>
{isRegister && (
<p className="text-xs text-muted-foreground leading-relaxed">
Used only to send you updates about your submissions
</p>
<p className="text-xs text-muted-foreground leading-relaxed">Used only to send you updates about your submissions</p>
)}
</div>
@@ -177,9 +168,7 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
className="h-11 text-base"
required
/>
<p className="text-xs text-muted-foreground leading-relaxed">
This will be displayed publicly with your submissions
</p>
<p className="text-xs text-muted-foreground leading-relaxed">This will be displayed publicly with your submissions</p>
</div>
)}
@@ -225,16 +214,16 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
</fieldset>
<footer className="space-y-4 pt-2">
<Button
type="submit"
className="w-full h-11 text-base font-semibold shadow-sm"
disabled={isLoading}
>
<Button type="submit" className="w-full h-11 text-base font-semibold shadow-sm" disabled={isLoading}>
{isLoading ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Please wait...
</>
@@ -263,4 +252,3 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
</Dialog>
)
}

View File

@@ -1,18 +1,21 @@
'use client'
"use client"
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { useState } from 'react'
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import { useState } from "react"
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
},
},
}))
}),
)
return (
<QueryClientProvider client={queryClient}>
@@ -21,5 +24,3 @@ export function Providers({ children }: { children: React.ReactNode }) {
</QueryClientProvider>
)
}

View File

@@ -1,21 +1,21 @@
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { Button } from "@/components/ui/button"
import { Calendar, User as UserIcon, FileType, Tag, FolderOpen, Palette, ExternalLink, Download, Check, X } from "lucide-react"
import { MagicCard } from "@/components/magicui/magic-card"
import { Calendar, Check, Download, ExternalLink, FileType, FolderOpen, Palette, Tag, User as UserIcon, X } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { IconCard } from "@/components/icon-card"
import { MagicCard } from "@/components/magicui/magic-card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
import { UserDisplay } from "@/components/user-display"
import { pb, type Submission, type User } from "@/lib/pb"
import { formatIconName } from "@/lib/utils"
import type { Icon } from "@/types/icons"
import { pb, type Submission, type User } from "@/lib/pb"
import Link from "next/link"
import Image from "next/image"
import { UserDisplay } from "@/components/user-display"
// Utility function to get display name with priority: username > email > created_by field
const getDisplayName = (submission: Submission, expandedData?: { created_by: User, approved_by: User }): string => {
const getDisplayName = (submission: Submission, expandedData?: { created_by: User; approved_by: User }): string => {
// Check if we have expanded user data
if (expandedData && expandedData.created_by) {
const user = expandedData.created_by
@@ -43,7 +43,15 @@ interface SubmissionDetailsProps {
isRejecting?: boolean
}
export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove, onReject, isApproving, isRejecting }: SubmissionDetailsProps) {
export function SubmissionDetails({
submission,
isAdmin,
onUserClick,
onApprove,
onReject,
isApproving,
isRejecting,
}: SubmissionDetailsProps) {
const expandedData = submission.expand
const displayName = getDisplayName(submission, expandedData)
@@ -53,7 +61,7 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
aliases: submission.extras?.aliases || [],
categories: submission.extras?.categories || [],
colors: submission.extras?.colors || null,
wordmark: submission.extras?.wordmark || null
wordmark: submission.extras?.wordmark || null,
}
const formattedCreated = new Date(submission.created).toLocaleDateString("en-GB", {
@@ -81,17 +89,21 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
timestamp: submission.updated,
author: {
id: 1,
name: displayName
}
name: displayName,
},
colors: sanitizedExtras.colors ? {
},
colors: sanitizedExtras.colors
? {
dark: sanitizedExtras.colors.dark,
light: sanitizedExtras.colors.light
} : undefined,
wordmark: sanitizedExtras.wordmark ? {
light: sanitizedExtras.colors.light,
}
: undefined,
wordmark: sanitizedExtras.wordmark
? {
dark: sanitizedExtras.wordmark.dark,
light: sanitizedExtras.wordmark.light
} : undefined
light: sanitizedExtras.wordmark.light,
}
: undefined,
}
const handleDownload = async (url: string, filename: string) => {
@@ -143,7 +155,7 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation()
window.open(`${pb.baseUrl}/api/files/submissions/${submission.id}/${asset}`, '_blank')
window.open(`${pb.baseUrl}/api/files/submissions/${submission.id}/${asset}`, "_blank")
}}
>
<ExternalLink className="h-3 w-3" />
@@ -154,7 +166,10 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation()
handleDownload(`${pb.baseUrl}/api/files/submissions/${submission.id}/${asset}`, `${submission.name}-${index + 1}.${sanitizedExtras.base}`)
handleDownload(
`${pb.baseUrl}/api/files/submissions/${submission.id}/${asset}`,
`${submission.name}-${index + 1}.${sanitizedExtras.base}`,
)
}}
>
<Download className="h-3 w-3" />
@@ -163,11 +178,7 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
</div>
</MagicCard>
))}
{submission.assets.length === 0 && (
<div className="text-center py-8 text-muted-foreground text-sm">
No assets available
</div>
)}
{submission.assets.length === 0 && <div className="text-center py-8 text-muted-foreground text-sm">No assets available</div>}
</div>
</CardContent>
</Card>
@@ -239,7 +250,7 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
</Badge>
</div>
{sanitizedExtras.colors && (Object.keys(sanitizedExtras.colors).length > 0) && (
{sanitizedExtras.colors && Object.keys(sanitizedExtras.colors).length > 0 && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2 flex items-center gap-2">
<Palette className="w-4 h-4" />
@@ -249,24 +260,20 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
{sanitizedExtras.colors.dark && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground min-w-12">Dark:</span>
<code className="text-xs bg-muted px-2 py-1 rounded font-mono">
{sanitizedExtras.colors.dark}
</code>
<code className="text-xs bg-muted px-2 py-1 rounded font-mono">{sanitizedExtras.colors.dark}</code>
</div>
)}
{sanitizedExtras.colors.light && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground min-w-12">Light:</span>
<code className="text-xs bg-muted px-2 py-1 rounded font-mono">
{sanitizedExtras.colors.light}
</code>
<code className="text-xs bg-muted px-2 py-1 rounded font-mono">{sanitizedExtras.colors.light}</code>
</div>
)}
</div>
</div>
)}
{sanitizedExtras.wordmark && (Object.keys(sanitizedExtras.wordmark).length > 0) && (
{sanitizedExtras.wordmark && Object.keys(sanitizedExtras.wordmark).length > 0 && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2 flex items-center gap-2">
<FileType className="w-4 h-4" />
@@ -276,17 +283,13 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
{sanitizedExtras.wordmark.dark && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground min-w-12">Dark:</span>
<code className="text-xs bg-muted px-2 py-1 rounded font-mono">
{sanitizedExtras.wordmark.dark}
</code>
<code className="text-xs bg-muted px-2 py-1 rounded font-mono">{sanitizedExtras.wordmark.dark}</code>
</div>
)}
{sanitizedExtras.wordmark.light && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground min-w-12">Light:</span>
<code className="text-xs bg-muted px-2 py-1 rounded font-mono">
{sanitizedExtras.wordmark.light}
</code>
<code className="text-xs bg-muted px-2 py-1 rounded font-mono">{sanitizedExtras.wordmark.light}</code>
</div>
)}
</div>
@@ -312,7 +315,7 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2 flex items-center gap-2">
<UserIcon className="w-4 h-4" />
{submission.status === 'approved' ? 'Approved By' : 'Reviewed By'}
{submission.status === "approved" ? "Approved By" : "Reviewed By"}
</h3>
<UserDisplay
@@ -389,9 +392,7 @@ export function SubmissionDetails({ submission, isAdmin, onUserClick, onApprove,
{isAdmin && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Submission ID</h3>
<code className="bg-muted px-2 py-1 rounded block break-all font-mono">
{submission.id}
</code>
<code className="bg-muted px-2 py-1 rounded block break-all font-mono">{submission.id}</code>
</div>
)}
</div>

View File

@@ -1,57 +1,57 @@
"use client"
import * as React from "react"
import {
type ColumnDef,
type ColumnFiltersState,
type ExpandedState,
flexRender,
getCoreRowModel,
useReactTable,
getSortedRowModel,
type SortingState,
type ExpandedState,
getExpandedRowModel,
getFilteredRowModel,
type ColumnFiltersState,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { Check, ChevronDown, ChevronRight, Filter, ImageIcon, Search, SortDesc, X } from "lucide-react"
import * as React from "react"
import { SubmissionDetails } from "@/components/submission-details"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ChevronDown, ChevronRight, ImageIcon, Check, X, Search, Filter, SortDesc } from "lucide-react"
import { SubmissionDetails } from "@/components/submission-details"
import { cn } from "@/lib/utils"
import { pb, type Submission } from "@/lib/pb"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { UserDisplay } from "@/components/user-display"
import { pb, type Submission } from "@/lib/pb"
import { cn } from "@/lib/utils"
// Initialize dayjs relative time plugin
dayjs.extend(relativeTime)
// Utility function to get display name with priority: username > email > created_by field
const getDisplayName = (submission: Submission, expandedData?: any): string => {
console.log('🏷️ Getting display name for submission:', submission.id)
console.log('👤 created_by field:', submission.created_by)
console.log('🔗 expanded data:', expandedData)
console.log("🏷️ Getting display name for submission:", submission.id)
console.log("👤 created_by field:", submission.created_by)
console.log("🔗 expanded data:", expandedData)
// Check if we have expanded user data
if (expandedData && expandedData.created_by) {
const user = expandedData.created_by
console.log('📋 User data from expand:', user)
console.log("📋 User data from expand:", user)
// Priority: username > email
if (user.username) {
console.log('✅ Using username:', user.username)
console.log("✅ Using username:", user.username)
return user.username
}
if (user.email) {
console.log('✅ Using email:', user.email)
console.log("✅ Using email:", user.email)
return user.email
}
}
// Fallback to created_by field (could be user ID or username)
console.log('⚠️ Fallback to created_by field:', submission.created_by)
console.log("⚠️ Fallback to created_by field:", submission.created_by)
return submission.created_by
}
@@ -109,7 +109,15 @@ const getStatusDisplayName = (status: Submission["status"]) => {
}
}
export function SubmissionsDataTable({ data, isAdmin, currentUserId, onApprove, onReject, isApproving, isRejecting }: SubmissionsDataTableProps) {
export function SubmissionsDataTable({
data,
isAdmin,
currentUserId,
onApprove,
onReject,
isApproving,
isRejecting,
}: SubmissionsDataTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [expanded, setExpanded] = React.useState<ExpandedState>({})
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
@@ -127,18 +135,18 @@ export function SubmissionsDataTable({ data, isAdmin, currentUserId, onApprove,
}, [data])
// Handle user filter - filter by user ID but display username
const handleUserFilter = React.useCallback((userId: string, displayName: string) => {
const handleUserFilter = React.useCallback(
(userId: string, displayName: string) => {
if (userFilter?.userId === userId) {
setUserFilter(null)
setColumnFilters(prev => prev.filter(filter => filter.id !== "created_by"))
setColumnFilters((prev) => prev.filter((filter) => filter.id !== "created_by"))
} else {
setUserFilter({ userId, displayName })
setColumnFilters(prev => [
...prev.filter(filter => filter.id !== "created_by"),
{ id: "created_by", value: userId }
])
setColumnFilters((prev) => [...prev.filter((filter) => filter.id !== "created_by"), { id: "created_by", value: userId }])
}
}, [userFilter])
},
[userFilter],
)
const columns: ColumnDef<Submission>[] = [
{
@@ -230,9 +238,7 @@ export function SubmissionsDataTable({ data, isAdmin, currentUserId, onApprove,
onClick={handleUserFilter}
size="md"
/>
{userFilter?.userId === userId && (
<X className="h-3 w-3 text-muted-foreground" />
)}
{userFilter?.userId === userId && <X className="h-3 w-3 text-muted-foreground" />}
</div>
)
},
@@ -345,7 +351,7 @@ export function SubmissionsDataTable({ data, isAdmin, currentUserId, onApprove,
className="h-auto p-0 hover:bg-transparent"
onClick={() => {
setUserFilter(null)
setColumnFilters(prev => prev.filter(filter => filter.id !== "created_by"))
setColumnFilters((prev) => prev.filter((filter) => filter.id !== "created_by"))
}}
>
<X className="h-3 w-3" />
@@ -390,8 +396,10 @@ export function SubmissionsDataTable({ data, isAdmin, currentUserId, onApprove,
{getStatusDisplayName(currentStatus)}
</Badge>
<span className="text-xs text-muted-foreground">
{table.getRowModel().rows.filter(r => r.original.status === currentStatus).length}
{table.getRowModel().rows.filter(r => r.original.status === currentStatus).length === 1 ? ' submission' : ' submissions'}
{table.getRowModel().rows.filter((r) => r.original.status === currentStatus).length}
{table.getRowModel().rows.filter((r) => r.original.status === currentStatus).length === 1
? " submission"
: " submissions"}
</span>
</div>
</TableCell>
@@ -399,10 +407,7 @@ export function SubmissionsDataTable({ data, isAdmin, currentUserId, onApprove,
)}
<TableRow
data-state={row.getIsSelected() && "selected"}
className={cn(
"cursor-pointer hover:bg-muted/50 transition-colors",
row.getIsExpanded() && "bg-muted/30"
)}
className={cn("cursor-pointer hover:bg-muted/50 transition-colors", row.getIsExpanded() && "bg-muted/30")}
onClick={() => handleRowToggle(row.id, row.getIsExpanded())}
>
{row.getVisibleCells().map((cell) => (

View File

@@ -1,39 +1,30 @@
"use client";
"use client"
import { Github, LogOut, User, LayoutDashboard } from "lucide-react";
import type React from "react";
import { useState } from "react";
import Link from "next/link";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { pb } from "@/lib/pb";
import { Github, LayoutDashboard, LogOut, User } from "lucide-react"
import Link from "next/link"
import type React from "react"
import { useState } from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { pb } from "@/lib/pb"
interface UserData {
username: string;
email: string;
avatar?: string;
username: string
email: string
avatar?: string
}
interface UserButtonProps {
asChild?: boolean;
isLoggedIn?: boolean;
userData?: UserData;
asChild?: boolean
isLoggedIn?: boolean
userData?: UserData
}
export function UserButton({
asChild,
isLoggedIn = false,
userData,
}: UserButtonProps) {
export function UserButton({ asChild, isLoggedIn = false, userData }: UserButtonProps) {
return (
<DropdownMenuTrigger asChild>
<Button
@@ -43,13 +34,8 @@ export function UserButton({
>
{isLoggedIn && userData ? (
<Avatar className="h-8 w-8">
<AvatarImage
src={userData.avatar || "/placeholder.svg"}
alt={userData.username}
/>
<AvatarFallback className="text-xs">
{userData.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
<AvatarImage src={userData.avatar || "/placeholder.svg"} alt={userData.username} />
<AvatarFallback className="text-xs">{userData.username.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
) : (
<User className="h-[1.2rem] w-[1.2rem] transition-all group-hover:scale-110" />
@@ -57,12 +43,12 @@ export function UserButton({
<span className="sr-only">{isLoggedIn ? "User menu" : "Sign in"}</span>
</Button>
</DropdownMenuTrigger>
);
)
}
interface UserMenuProps {
userData: UserData;
onSignOut: () => void;
userData: UserData
onSignOut: () => void
}
export function UserMenu({ userData, onSignOut }: UserMenuProps) {
@@ -70,29 +56,18 @@ export function UserMenu({ userData, onSignOut }: UserMenuProps) {
<div className="space-y-3">
<div className="flex items-center gap-3 px-1">
<Avatar className="h-10 w-10">
<AvatarImage
src={userData.avatar || "/placeholder.svg"}
alt={userData.username}
/>
<AvatarFallback className="text-sm font-semibold">
{userData.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
<AvatarImage src={userData.avatar || "/placeholder.svg"} alt={userData.username} />
<AvatarFallback className="text-sm font-semibold">{userData.username.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
<p className="text-sm font-semibold truncate">{userData.username}</p>
<p className="text-xs text-muted-foreground truncate">
{userData.email}
</p>
<p className="text-xs text-muted-foreground truncate">{userData.email}</p>
</div>
</div>
<DropdownMenuSeparator />
<Button
asChild
variant="ghost"
className="w-full justify-start gap-2 hover:bg-muted"
>
<Button asChild variant="ghost" className="w-full justify-start gap-2 hover:bg-muted">
<Link href="/dashboard">
<LayoutDashboard className="h-4 w-4" />
Dashboard
@@ -108,102 +83,96 @@ export function UserMenu({ userData, onSignOut }: UserMenuProps) {
<div>
<LogOut className="h-4 w-4" />
Sign out
</div>
</Button>
</div>
);
)
}
interface LoginPopupProps {
trigger?: React.ReactNode;
isLoggedIn?: boolean;
userData?: UserData;
onSignOut?: () => void;
trigger?: React.ReactNode
isLoggedIn?: boolean
userData?: UserData
onSignOut?: () => void
}
export function LoginPopup({
trigger,
isLoggedIn = false,
userData,
onSignOut,
}: LoginPopupProps) {
const [open, setOpen] = useState(false);
const [isRegister, setIsRegister] = useState(false);
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
export function LoginPopup({ trigger, isLoggedIn = false, userData, onSignOut }: LoginPopupProps) {
const [open, setOpen] = useState(false)
const [isRegister, setIsRegister] = useState(false)
const [email, setEmail] = useState("")
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [error, setError] = useState("")
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setIsLoading(true);
e.preventDefault()
setError("")
setIsLoading(true)
try {
if (isRegister) {
if (password !== confirmPassword) {
setError("Passwords do not match");
setIsLoading(false);
return;
setError("Passwords do not match")
setIsLoading(false)
return
}
if (!username.trim()) {
setError("Username is required");
setIsLoading(false);
return;
setError("Username is required")
setIsLoading(false)
return
}
if (!email.trim()) {
setError("Email is required");
setIsLoading(false);
return;
setError("Email is required")
setIsLoading(false)
return
}
await pb.collection('users').create({
await pb.collection("users").create({
username: username.trim(),
email: email.trim(),
password,
passwordConfirm: confirmPassword,
});
})
await pb.collection('users').authWithPassword(email, password);
await pb.collection("users").authWithPassword(email, password)
} else {
// For login, use email as the identifier
await pb.collection('users').authWithPassword(email, password);
await pb.collection("users").authWithPassword(email, password)
}
setOpen(false);
setEmail("");
setUsername("");
setPassword("");
setConfirmPassword("");
setOpen(false)
setEmail("")
setUsername("")
setPassword("")
setConfirmPassword("")
} catch (err: any) {
console.error('Auth error:', err);
setError(err?.message || "Authentication failed. Please try again.");
console.error("Auth error:", err)
setError(err?.message || "Authentication failed. Please try again.")
} finally {
setIsLoading(false);
setIsLoading(false)
}
}
};
const toggleMode = () => {
setIsRegister(!isRegister);
setEmail("");
setUsername("");
setPassword("");
setConfirmPassword("");
setError("");
};
setIsRegister(!isRegister)
setEmail("")
setUsername("")
setPassword("")
setConfirmPassword("")
setError("")
}
const handleSignOut = () => {
setOpen(false);
setOpen(false)
// Wait for dropdown close animation before updating parent state
setTimeout(() => {
onSignOut?.();
}, 150);
};
onSignOut?.()
}, 150)
}
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
@@ -214,19 +183,11 @@ export function LoginPopup({
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<h3 className="font-semibold text-lg">
{isRegister ? "Create account" : "Sign in"}
</h3>
<h3 className="font-semibold text-lg">{isRegister ? "Create account" : "Sign in"}</h3>
<p className="text-sm text-muted-foreground">
{isRegister
? "Enter your details to create an account"
: "Enter your credentials to continue"}
{isRegister ? "Enter your details to create an account" : "Enter your credentials to continue"}
</p>
{error && (
<div className="text-sm text-destructive bg-destructive/10 px-3 py-2 rounded-md">
{error}
</div>
)}
{error && <div className="text-sm text-destructive bg-destructive/10 px-3 py-2 rounded-md">{error}</div>}
</div>
<div
@@ -260,11 +221,7 @@ export function LoginPopup({
onChange={(e) => setEmail(e.target.value)}
required
/>
{isRegister && (
<p className="text-xs text-muted-foreground">
Used only to send you updates about your submissions
</p>
)}
{isRegister && <p className="text-xs text-muted-foreground">Used only to send you updates about your submissions</p>}
</div>
{isRegister && (
@@ -281,9 +238,7 @@ export function LoginPopup({
onChange={(e) => setUsername(e.target.value)}
required
/>
<p className="text-xs text-muted-foreground">
This will be displayed publicly with your submissions
</p>
<p className="text-xs text-muted-foreground">This will be displayed publicly with your submissions</p>
</div>
)}
@@ -322,7 +277,7 @@ export function LoginPopup({
<footer className="space-y-3">
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Please wait..." : (isRegister ? "Register" : "Login")}
{isLoading ? "Please wait..." : isRegister ? "Register" : "Login"}
</Button>
<div className="text-center text-sm">
@@ -339,5 +294,5 @@ export function LoginPopup({
)}
</DropdownMenuContent>
</DropdownMenu>
);
)
}

View File

@@ -16,23 +16,16 @@ interface UserDisplayProps {
const sizeClasses = {
sm: "h-6 w-6",
md: "h-8 w-8",
lg: "h-10 w-10"
lg: "h-10 w-10",
}
const textSizeClasses = {
sm: "text-xs",
md: "text-sm",
lg: "text-sm"
lg: "text-sm",
}
export function UserDisplay({
userId,
avatar,
displayName,
onClick,
size = "sm",
showAvatar = true
}: UserDisplayProps) {
export function UserDisplay({ userId, avatar, displayName, onClick, size = "sm", showAvatar = true }: UserDisplayProps) {
// Avatar URL will attempt to load from PocketBase
// If it doesn't exist, the AvatarFallback will display instead
const avatarUrl = userId ? `${pb.baseURL}/api/files/_pb_users_auth_/${userId}/${avatar}?thumb=100x100` : undefined
@@ -42,9 +35,7 @@ export function UserDisplay({
{showAvatar && (
<Avatar className={sizeClasses[size]}>
{avatarUrl && <AvatarImage src={avatarUrl} alt={displayName} />}
<AvatarFallback className={textSizeClasses[size]}>
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
<AvatarFallback className={textSizeClasses[size]}>{displayName.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
)}
{onClick && userId ? (
@@ -64,4 +55,3 @@ export function UserDisplay({
</div>
)
}

View File

@@ -1,11 +1,11 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { pb, type Submission } from '@/lib/pb'
import { toast } from 'sonner'
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import { pb, type Submission } from "@/lib/pb"
// Query key factory
export const submissionKeys = {
all: ['submissions'] as const,
lists: () => [...submissionKeys.all, 'list'] as const,
all: ["submissions"] as const,
lists: () => [...submissionKeys.all, "list"] as const,
list: (filters?: Record<string, any>) => [...submissionKeys.lists(), filters] as const,
}
@@ -14,18 +14,18 @@ export function useSubmissions() {
return useQuery({
queryKey: submissionKeys.lists(),
queryFn: async () => {
console.log('🔍 Fetching submissions...')
const records = await pb.collection('submissions').getFullList<Submission>({
sort: '-updated',
expand: 'created_by,approved_by',
console.log("🔍 Fetching submissions...")
const records = await pb.collection("submissions").getFullList<Submission>({
sort: "-updated",
expand: "created_by,approved_by",
requestKey: null,
})
console.log('📊 Fetched submissions:', records.length)
console.log("📊 Fetched submissions:", records.length)
if (records.length > 0) {
console.log('📋 First submission sample:', records[0])
console.log('👤 First submission created_by field:', records[0].created_by)
console.log('🔗 First submission expand data:', (records[0] as any).expand)
console.log("📋 First submission sample:", records[0])
console.log("👤 First submission created_by field:", records[0].created_by)
console.log("🔗 First submission expand data:", (records[0] as any).expand)
}
return records
@@ -39,12 +39,16 @@ export function useApproveSubmission() {
return useMutation({
mutationFn: async (submissionId: string) => {
return await pb.collection('submissions').update(submissionId, {
status: 'approved',
approved_by: pb.authStore.record?.id || ''
}, {
requestKey: null
})
return await pb.collection("submissions").update(
submissionId,
{
status: "approved",
approved_by: pb.authStore.record?.id || "",
},
{
requestKey: null,
},
)
},
onSuccess: (data) => {
// Invalidate and refetch submissions
@@ -56,7 +60,7 @@ export function useApproveSubmission() {
},
onError: (error: any) => {
console.error("Error approving submission:", error)
if (!error.message?.includes('autocancelled') && !error.name?.includes('AbortError')) {
if (!error.message?.includes("autocancelled") && !error.name?.includes("AbortError")) {
toast.error("Failed to approve submission", {
description: error.message || "An error occurred",
})
@@ -71,12 +75,16 @@ export function useRejectSubmission() {
return useMutation({
mutationFn: async (submissionId: string) => {
return await pb.collection('submissions').update(submissionId, {
status: 'rejected',
approved_by: pb.authStore.record?.id || ''
}, {
requestKey: null
})
return await pb.collection("submissions").update(
submissionId,
{
status: "rejected",
approved_by: pb.authStore.record?.id || "",
},
{
requestKey: null,
},
)
},
onSuccess: () => {
// Invalidate and refetch submissions
@@ -88,7 +96,7 @@ export function useRejectSubmission() {
},
onError: (error: any) => {
console.error("Error rejecting submission:", error)
if (!error.message?.includes('autocancelled') && !error.name?.includes('AbortError')) {
if (!error.message?.includes("autocancelled") && !error.name?.includes("AbortError")) {
toast.error("Failed to reject submission", {
description: error.message || "An error occurred",
})
@@ -100,7 +108,7 @@ export function useRejectSubmission() {
// Check authentication status
export function useAuth() {
return useQuery({
queryKey: ['auth'],
queryKey: ["auth"],
queryFn: async () => {
const isValid = pb.authStore.isValid
const userId = pb.authStore.record?.id
@@ -109,13 +117,13 @@ export function useAuth() {
return {
isAuthenticated: false,
isAdmin: false,
userId: '',
userId: "",
}
}
try {
// Fetch the full user record to get the admin status
const user = await pb.collection('users').getOne(userId, {
const user = await pb.collection("users").getOne(userId, {
requestKey: null,
})
@@ -125,11 +133,11 @@ export function useAuth() {
userId: userId,
}
} catch (error) {
console.error('Error fetching user:', error)
console.error("Error fetching user:", error)
return {
isAuthenticated: isValid,
isAdmin: false,
userId: userId || '',
userId: userId || "",
}
}
},
@@ -137,4 +145,3 @@ export function useAuth() {
retry: false,
})
}

View File

@@ -12,18 +12,16 @@ import type { IconWithName } from "@/types/icons"
* Note: Do not use the client-side pb instance (with auth store) on the server
*/
function createServerPB() {
return new PocketBase(process.env.POCKETBASE_URL || "http://127.0.0.1:8090")
return new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090")
}
/**
* Transform a CommunityGallery item to IconWithName format for use with IconSearch
*/
function transformGalleryToIcon(item: CommunityGallery): any {
const pbUrl = process.env.POCKETBASE_URL || "http://127.0.0.1:8090"
const pbUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090"
const fileUrl = item.assets?.[0]
? `${pbUrl}/api/files/community_gallery/${item.id}/${item.assets[0]}`
: ""
const fileUrl = item.assets?.[0] ? `${pbUrl}/api/files/community_gallery/${item.id}/${item.assets[0]}` : ""
const transformed = {
name: item.name,
@@ -62,9 +60,7 @@ export async function fetchCommunitySubmissions(): Promise<IconWithName[]> {
sort: "-created",
})
return records
.filter(item => item.assets && item.assets.length > 0)
.map(transformGalleryToIcon)
return records.filter((item) => item.assets && item.assets.length > 0).map(transformGalleryToIcon)
} catch (error) {
console.error("Error fetching community submissions:", error)
return []
@@ -80,4 +76,3 @@ export const getCommunitySubmissions = unstable_cache(fetchCommunitySubmissions,
revalidate: 600,
tags: ["community-gallery"],
})

View File

@@ -1,4 +1,4 @@
import PocketBase, { RecordService } from 'pocketbase';
import PocketBase, { type RecordService } from "pocketbase"
export interface User {
id: string
@@ -15,7 +15,7 @@ export interface Submission {
name: string
assets: string[]
created_by: string
status: 'approved' | 'rejected' | 'pending' | 'added_to_collection'
status: "approved" | "rejected" | "pending" | "added_to_collection"
approved_by: string
expand: {
created_by: User
@@ -42,7 +42,7 @@ export interface CommunityGallery {
id: string
name: string
created_by: string
status: 'approved' | 'rejected' | 'pending' | 'added_to_collection'
status: "approved" | "rejected" | "pending" | "added_to_collection"
assets: string[]
created: string
updated: string
@@ -63,10 +63,9 @@ export interface CommunityGallery {
interface TypedPocketBase extends PocketBase {
collection(idOrName: string): RecordService // default fallback for any other collection
collection(idOrName: 'users'): RecordService<User>
collection(idOrName: 'submissions'): RecordService<Submission>
collection(idOrName: 'community_gallery'): RecordService<CommunityGallery>
collection(idOrName: "users"): RecordService<User>
collection(idOrName: "submissions"): RecordService<Submission>
collection(idOrName: "community_gallery"): RecordService<CommunityGallery>
}
export const pb = new PocketBase('http://127.0.0.1:8090') as TypedPocketBase;
export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090") as TypedPocketBase

View File

@@ -19,4 +19,3 @@ export async function revalidateSubmissions() {
revalidatePath("/community")
revalidatePath("/dashboard")
}