mirror of
https://github.com/walkxcode/dashboard-icons.git
synced 2025-11-27 07:08:41 +01:00
Merge pull request #2512 from homarr-labs/feat/improve-community
This commit is contained in:
@@ -3,14 +3,7 @@ import type { NextConfig } from "next";
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: false,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
new URL(
|
||||
"https://pb.dashboardicons.com/**",
|
||||
),
|
||||
new URL(
|
||||
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/**",
|
||||
),
|
||||
],
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import { permanentRedirect, redirect } from "next/navigation"
|
||||
import { ImageResponse } from "next/og"
|
||||
import { getCommunityGalleryRecord, getCommunitySubmissionByName, getCommunitySubmissions } from "@/lib/community"
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
}
|
||||
|
||||
export const alt = "Community Icon Open Graph Image";
|
||||
export const contentType = "image/png";
|
||||
|
||||
export default async function Image({ params }: { params: Promise<{ icon: string }> }) {
|
||||
const { icon } = await params
|
||||
|
||||
if (!icon) {
|
||||
return new ImageResponse(
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "white",
|
||||
fontSize: 48,
|
||||
fontWeight: 600,
|
||||
color: "#64748b",
|
||||
}}
|
||||
>
|
||||
Icon not found
|
||||
</div>,
|
||||
{ ...size },
|
||||
)
|
||||
}
|
||||
|
||||
const iconData = await getCommunitySubmissionByName(icon)
|
||||
|
||||
if (!iconData) {
|
||||
return new ImageResponse(
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "white",
|
||||
fontSize: 48,
|
||||
fontWeight: 600,
|
||||
color: "#64748b",
|
||||
}}
|
||||
>
|
||||
Icon not found
|
||||
</div>,
|
||||
{ ...size },
|
||||
)
|
||||
}
|
||||
|
||||
const record = await getCommunityGalleryRecord(icon)
|
||||
if (record?.status === "added_to_collection") {
|
||||
permanentRedirect(`/icons/${icon}/opengraph-image`)
|
||||
}
|
||||
|
||||
const status = record?.status || "pending"
|
||||
const allIcons = await getCommunitySubmissions()
|
||||
const totalIcons = allIcons.length
|
||||
const index = allIcons.findIndex((i) => i.name === icon)
|
||||
|
||||
const formattedIconName = icon
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ")
|
||||
|
||||
const getStatusDisplayName = (status: string) => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "Awaiting Review"
|
||||
case "approved":
|
||||
return "Approved"
|
||||
case "rejected":
|
||||
return "Rejected"
|
||||
case "added_to_collection":
|
||||
return "Added to Collection"
|
||||
default:
|
||||
return "Awaiting Review"
|
||||
}
|
||||
}
|
||||
|
||||
const statusColors: Record<string, { bg: string; text: string; border: string }> = {
|
||||
approved: {
|
||||
bg: "#dbeafe",
|
||||
text: "#1e40af",
|
||||
border: "#93c5fd",
|
||||
},
|
||||
pending: {
|
||||
bg: "#fef3c7",
|
||||
text: "#92400e",
|
||||
border: "#fde68a",
|
||||
},
|
||||
rejected: {
|
||||
bg: "#fee2e2",
|
||||
text: "#991b1b",
|
||||
border: "#fca5a5",
|
||||
},
|
||||
}
|
||||
|
||||
const statusConfig = statusColors[status] || statusColors.pending
|
||||
const statusLabel = getStatusDisplayName(status)
|
||||
|
||||
const mainIconUrl = typeof iconData.data.base === "string" && iconData.data.base.startsWith("http") ? iconData.data.base : null
|
||||
|
||||
let iconDataBuffer: Buffer | null = null
|
||||
if (mainIconUrl) {
|
||||
try {
|
||||
const response = await fetch(mainIconUrl)
|
||||
if (response.ok) {
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
iconDataBuffer = Buffer.from(arrayBuffer)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch icon image for ${icon}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const iconUrl = iconDataBuffer
|
||||
? `data:image/png;base64,${iconDataBuffer.toString("base64")}`
|
||||
: `https://placehold.co/600x400?text=${formattedIconName}`;
|
||||
|
||||
return new ImageResponse(
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
fontFamily: "Inter, system-ui, sans-serif",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "white",
|
||||
backgroundImage:
|
||||
"radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%), radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%)",
|
||||
backgroundSize: "100px 100px",
|
||||
}}
|
||||
>
|
||||
{/* Status Badge */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 30,
|
||||
right: 30,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: statusConfig.bg,
|
||||
color: statusConfig.text,
|
||||
border: `2px solid ${statusConfig.border}`,
|
||||
borderRadius: 12,
|
||||
padding: "10px 20px",
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.5px",
|
||||
boxShadow:
|
||||
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
|
||||
zIndex: 30,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{statusLabel}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -100,
|
||||
left: -100,
|
||||
width: 400,
|
||||
height: 400,
|
||||
borderRadius: "50%",
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(56, 189, 248, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)",
|
||||
filter: "blur(80px)",
|
||||
zIndex: 2,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: -150,
|
||||
right: -150,
|
||||
width: 500,
|
||||
height: 500,
|
||||
borderRadius: "50%",
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(249, 115, 22, 0.1) 0%, rgba(234, 88, 12, 0.1) 100%)",
|
||||
filter: "blur(100px)",
|
||||
zIndex: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: "60px",
|
||||
gap: "70px",
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 320,
|
||||
height: 320,
|
||||
borderRadius: 32,
|
||||
background: "white",
|
||||
boxShadow:
|
||||
"0 25px 50px -12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)",
|
||||
padding: 30,
|
||||
flexShrink: 0,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: "linear-gradient(145deg, #ffffff 0%, #f8fafc 100%)",
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={iconUrl}
|
||||
alt={formattedIconName}
|
||||
width={260}
|
||||
height={260}
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
filter: "drop-shadow(0 10px 15px rgba(0, 0, 0, 0.1))",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
gap: 28,
|
||||
maxWidth: 650,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "#0f172a",
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: "-0.02em",
|
||||
}}
|
||||
>
|
||||
Download {formattedIconName} icon (Community)
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: 32,
|
||||
fontWeight: 500,
|
||||
color: "#64748b",
|
||||
lineHeight: 1.4,
|
||||
position: "relative",
|
||||
paddingLeft: 16,
|
||||
borderLeft: "4px solid #94a3b8",
|
||||
}}
|
||||
>
|
||||
Amongst {totalIcons} other community-submitted icons
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#fef3c7",
|
||||
color: "#92400e",
|
||||
border: "2px solid #fde68a",
|
||||
borderRadius: 12,
|
||||
padding: "8px 16px",
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)",
|
||||
}}
|
||||
>
|
||||
COMMUNITY
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 80,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#ffffff",
|
||||
borderTop: "2px solid rgba(0, 0, 0, 0.05)",
|
||||
zIndex: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
color: "#334155",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#3b82f6",
|
||||
marginRight: 4,
|
||||
}}
|
||||
/>
|
||||
dashboardicons.com
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
...size,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -165,6 +165,7 @@ export default async function CommunityIconPage({ params }: { params: Promise<{
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: Needs to be done
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
|
||||
@@ -3,6 +3,22 @@ import { join } from "node:path";
|
||||
import { ImageResponse } from "next/og";
|
||||
import { getAllIcons } from "@/lib/api";
|
||||
|
||||
export const revalidate = false;
|
||||
export async function generateStaticParams() {
|
||||
const iconsData = await getAllIcons();
|
||||
if (process.env.CI_MODE === "false") {
|
||||
// This is meant to speed up the build process in local development
|
||||
return Object.keys(iconsData)
|
||||
.slice(0, 5)
|
||||
.map((icon) => ({
|
||||
icon,
|
||||
}));
|
||||
}
|
||||
return Object.keys(iconsData).map((icon) => ({
|
||||
icon,
|
||||
}));
|
||||
}
|
||||
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
@@ -151,11 +167,10 @@ export default async function Image({
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
{iconUrl && (
|
||||
// biome-ignore lint/performance/noImgElement: Not using nextjs here
|
||||
<img
|
||||
src={
|
||||
iconUrl ||
|
||||
`https://placehold.co/600x400?text=${formattedIconName}`
|
||||
}
|
||||
src={iconUrl}
|
||||
alt={formattedIconName}
|
||||
width={260}
|
||||
height={260}
|
||||
@@ -166,6 +181,28 @@ export default async function Image({
|
||||
filter: "drop-shadow(0 10px 15px rgba(0, 0, 0, 0.1))",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!iconUrl && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 260,
|
||||
height: 260,
|
||||
backgroundColor: "#f1f5f9",
|
||||
color: "#475569",
|
||||
border: "2px solid #e2e8f0",
|
||||
borderRadius: 12,
|
||||
padding: "8px 16px",
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)",
|
||||
}}
|
||||
>
|
||||
{formattedIconName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Text content */}
|
||||
|
||||
@@ -1,46 +1,61 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Calendar, Check, Download, ExternalLink, FileType, FolderOpen, Palette, Tag, User as UserIcon, X, Eye } 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 {
|
||||
Calendar,
|
||||
Check,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
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";
|
||||
|
||||
// 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
|
||||
const user = expandedData.created_by;
|
||||
|
||||
// Priority: username > email
|
||||
if (user.username) {
|
||||
return user.username
|
||||
return user.username;
|
||||
}
|
||||
if (user.email) {
|
||||
return user.email
|
||||
return user.email;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to created_by field (could be user ID or username)
|
||||
return submission.created_by
|
||||
}
|
||||
return submission.created_by;
|
||||
};
|
||||
|
||||
interface SubmissionDetailsProps {
|
||||
submission: Submission
|
||||
isAdmin: boolean
|
||||
onUserClick?: (userId: string, displayName: string) => void
|
||||
onApprove?: () => void
|
||||
onReject?: () => void
|
||||
isApproving?: boolean
|
||||
isRejecting?: boolean
|
||||
submission: Submission;
|
||||
isAdmin: boolean;
|
||||
onUserClick?: (userId: string, displayName: string) => void;
|
||||
onApprove?: () => void;
|
||||
onReject?: () => void;
|
||||
isApproving?: boolean;
|
||||
isRejecting?: boolean;
|
||||
}
|
||||
|
||||
export function SubmissionDetails({
|
||||
@@ -52,8 +67,8 @@ export function SubmissionDetails({
|
||||
isApproving,
|
||||
isRejecting,
|
||||
}: SubmissionDetailsProps) {
|
||||
const expandedData = submission.expand
|
||||
const displayName = getDisplayName(submission, expandedData)
|
||||
const expandedData = submission.expand;
|
||||
const displayName = getDisplayName(submission, expandedData);
|
||||
|
||||
// Sanitize extras to ensure we have safe defaults
|
||||
const sanitizedExtras = {
|
||||
@@ -62,23 +77,29 @@ export function SubmissionDetails({
|
||||
categories: submission.extras?.categories || [],
|
||||
colors: submission.extras?.colors || null,
|
||||
wordmark: submission.extras?.wordmark || null,
|
||||
}
|
||||
};
|
||||
|
||||
const formattedCreated = new Date(submission.created).toLocaleDateString("en-GB", {
|
||||
const formattedCreated = new Date(submission.created).toLocaleDateString(
|
||||
"en-GB",
|
||||
{
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
const formattedUpdated = new Date(submission.updated).toLocaleDateString("en-GB", {
|
||||
const formattedUpdated = new Date(submission.updated).toLocaleDateString(
|
||||
"en-GB",
|
||||
{
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// Create a mock Icon object for the IconCard component
|
||||
const mockIconData: Icon = {
|
||||
@@ -104,24 +125,24 @@ export function SubmissionDetails({
|
||||
light: sanitizedExtras.wordmark.light,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async (url: string, filename: string) => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const blob = await response.blob()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const link = document.createElement("a")
|
||||
link.href = blobUrl
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 100)
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = blobUrl;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
|
||||
} catch (error) {
|
||||
console.error("Download error:", error)
|
||||
}
|
||||
console.error("Download error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
@@ -141,7 +162,10 @@ export function SubmissionDetails({
|
||||
<div className="relative">
|
||||
<div className="aspect-square rounded-lg border flex items-center justify-center p-8 bg-muted/30">
|
||||
<Image
|
||||
src={`${pb.baseURL}/api/files/submissions/${submission.id}/${asset}` || "/placeholder.svg"}
|
||||
src={
|
||||
`${pb.baseURL}/api/files/submissions/${submission.id}/${asset}` ||
|
||||
"/placeholder.svg"
|
||||
}
|
||||
alt={`${submission.name} asset ${index + 1}`}
|
||||
width={200}
|
||||
height={200}
|
||||
@@ -154,8 +178,11 @@ export function SubmissionDetails({
|
||||
variant="secondary"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
window.open(`${pb.baseUrl}/api/files/submissions/${submission.id}/${asset}`, "_blank")
|
||||
e.stopPropagation();
|
||||
window.open(
|
||||
`${pb.baseUrl}/api/files/submissions/${submission.id}/${asset}`,
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
@@ -165,11 +192,11 @@ export function SubmissionDetails({
|
||||
variant="secondary"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.stopPropagation();
|
||||
handleDownload(
|
||||
`${pb.baseUrl}/api/files/submissions/${submission.id}/${asset}`,
|
||||
`${submission.name}-${index + 1}.${sanitizedExtras.base}`,
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Download className="h-3 w-3" />
|
||||
@@ -178,7 +205,11 @@ export function SubmissionDetails({
|
||||
</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>
|
||||
@@ -208,8 +239,8 @@ export function SubmissionDetails({
|
||||
color="green"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onApprove()
|
||||
e.stopPropagation();
|
||||
onApprove();
|
||||
}}
|
||||
disabled={isApproving || isRejecting}
|
||||
>
|
||||
@@ -223,8 +254,8 @@ export function SubmissionDetails({
|
||||
color="red"
|
||||
variant="destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onReject()
|
||||
e.stopPropagation();
|
||||
onReject();
|
||||
}}
|
||||
disabled={isApproving || isRejecting}
|
||||
>
|
||||
@@ -244,8 +275,12 @@ export function SubmissionDetails({
|
||||
<FileType className="w-4 h-4" />
|
||||
Icon Name
|
||||
</h3>
|
||||
<p className="text-lg font-medium capitalize">{formatIconName(submission.name)}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">Filename: {submission.name}</p>
|
||||
<p className="text-lg font-medium capitalize">
|
||||
{formatIconName(submission.name)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Filename: {submission.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -258,7 +293,8 @@ export function SubmissionDetails({
|
||||
</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" />
|
||||
@@ -267,21 +303,30 @@ export function SubmissionDetails({
|
||||
<div className="space-y-2">
|
||||
{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>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</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" />
|
||||
@@ -290,14 +335,22 @@ export function SubmissionDetails({
|
||||
<div className="space-y-2">
|
||||
{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>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -323,7 +376,9 @@ export function SubmissionDetails({
|
||||
<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
|
||||
@@ -365,16 +420,25 @@ export function SubmissionDetails({
|
||||
{sanitizedExtras.categories.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{sanitizedExtras.categories.map((category) => (
|
||||
<Badge key={category} variant="outline" className="border-primary/20 hover:border-primary">
|
||||
<Badge
|
||||
key={category}
|
||||
variant="outline"
|
||||
className="border-primary/20 hover:border-primary"
|
||||
>
|
||||
{category
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.map(
|
||||
(word) =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1),
|
||||
)
|
||||
.join(" ")}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No categories assigned</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No categories assigned
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -386,21 +450,31 @@ export function SubmissionDetails({
|
||||
{sanitizedExtras.aliases.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{sanitizedExtras.aliases.map((alias) => (
|
||||
<Badge key={alias} variant="outline" className="text-xs font-mono">
|
||||
<Badge
|
||||
key={alias}
|
||||
variant="outline"
|
||||
className="text-xs font-mono"
|
||||
>
|
||||
{alias}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No aliases assigned</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No aliases assigned
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -408,5 +482,5 @@ export function SubmissionDetails({
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user