Basic layout finished

This commit is contained in:
2025-06-28 22:47:00 +02:00
parent 26b034f6f0
commit ee79f75668
32 changed files with 3474 additions and 5 deletions

View File

@ -0,0 +1,48 @@
// components/images/ArtistInfoBox.tsx
"use client"
import { Artist, Social } from "@/generated/prisma"
import { getSocialIcon } from "@/utils/socialIconMap"
import { UserIcon } from "lucide-react"
import Link from "next/link"
type ArtistWithItems = Artist & {
socials: Social[]
}
export default function ArtistInfoBox({ artist }: { artist: ArtistWithItems }) {
return (
<div className="border rounded-lg p-4 shadow bg-muted/30 w-full max-w-xl">
<div className="flex items-center gap-3 mb-2">
<UserIcon className="w-5 h-5 text-muted-foreground" />
<Link href={`/artists/${artist.slug}`} className="font-semibold text-lg hover:underline">
{artist.displayName}
</Link>
</div>
{artist.socials?.length > 0 && (
<div className="flex flex-col gap-3 flex-wrap mt-2">
{artist.socials.map((social) => {
const Icon = getSocialIcon(social.platform)
return (
<div key={social.id}>
<a
href={social.link || ""}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary hover:underline"
>
<Icon className="w-4 h-4" />
{social.platform}: {social.handle}
{social.isPrimary && (
<span className="text-xs text-primary font-semibold ml-1">(main)</span>
)}
</a>
</div>
)
})}
</div>
)}
</div>
)
}

View File

@ -0,0 +1,76 @@
"use client"
import { Color, ImageColor, ImageVariant } from "@/generated/prisma"
import clsx from "clsx"
import { useTheme } from "next-themes"
import NextImage from "next/image"
import { useEffect, useState } from "react"
type Colors = ImageColor & {
color: Color
}
type Props = {
alt: string,
variant: ImageVariant,
colors: Colors[],
src: string
className?: string
animate?: boolean
}
export default function GlowingImageBorder({
alt,
variant,
colors,
src,
className,
animate = true,
}: Props) {
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const getColor = (type: string) =>
colors.find((c) => c.type === type)?.color.hex
const vibrantLight = getColor("vibrant") || "#ff5ec4"
const mutedLight = getColor("muted") || "#5ecaff"
const darkVibrant = getColor("darkVibrant") || "#fc03a1"
const darkMuted = getColor("darkMuted") || "#035efc"
const vibrant = resolvedTheme === "dark" ? darkVibrant : vibrantLight
const muted = resolvedTheme === "dark" ? darkMuted : mutedLight
if (!mounted) return null;
return (
<div
className={clsx(
"relative inline-block rounded-xl overflow-hidden p-[12px]",
animate ? "glow-border" : "static-glow-border",
className
)}
style={
{
"--vibrant": vibrant,
"--muted": muted,
} as React.CSSProperties
}
>
<div className="relative z-10 rounded-xl overflow-hidden">
<NextImage
src={src}
alt={alt || "Image"}
width={variant.width}
height={variant.height}
className="rounded-xl"
/>
</div>
</div>
)
}

View File

@ -0,0 +1,37 @@
"use client"
import { Color, ImageColor, ImageVariant } from "@/generated/prisma";
import { useState } from "react";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import GlowingImageBorder from "./GlowingImageBorder";
type Props = {
variant: ImageVariant;
colors: (ImageColor & { color: Color })[];
alt: string;
src: string;
};
export default function GlowingImageWithToggle({ variant, colors, alt, src }: Props) {
const [animate, setAnimate] = useState(true);
return (
<div className="relative w-full max-w-fit">
<GlowingImageBorder
alt={alt}
variant={variant}
colors={colors}
src={src}
animate={animate}
/>
<div className="flex flex-col items-center gap-4 pt-8">
<div className="flex items-center gap-2">
<Switch id="animate" checked={animate} onCheckedChange={setAnimate} />
<Label htmlFor="animate">Animate glow</Label>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,100 @@
import { Image as ImageType } from "@/generated/prisma";
import Link from "next/link";
// import { SocialIcon } from "react-social-icons";
type Props = {
image: ImageType & {
artist?: {
id: string;
slug: string;
displayName: string;
socials?: { type: string; handle: string; link: string }[];
};
categories?: { id: string; name: string }[];
tags?: { id: string; name: string }[];
album?: { id: string; name: string; slug: string };
};
};
export default function ImageInfoPanel({ image }: Props) {
return (
<div className="w-full max-w-2xl mt-8 border border-border rounded-lg p-6 shadow-sm bg-background">
{/* Creator */}
{image.artist && (
<div className="mb-4">
<h2 className="text-lg font-semibold mb-1">Creator</h2>
<Link
href={`/artists/${image.artist.slug}`}
className="text-primary hover:underline font-medium"
>
{image.artist.displayName}
</Link>
{image.artist.socials?.length > 0 && (
<div className="flex gap-2 mt-2">
{image.artist.socials.map((social, i) => (
<SocialIcon
key={i}
url={social.link}
target="_blank"
rel="noopener noreferrer"
style={{ height: 28, width: 28 }}
title={social.type}
/>
))}
</div>
)}
</div>
)}
{/* Album */}
{image.album && (
<div className="mb-4">
<h2 className="text-lg font-semibold mb-1">Album</h2>
<Link
href={`/galleries/${image.album.slug}`}
className="text-primary hover:underline"
>
{image.album.name}
</Link>
</div>
)}
{/* Categories */}
{image.categories?.length > 0 && (
<div className="mb-4">
<h2 className="text-lg font-semibold mb-1">Categories</h2>
<div className="flex flex-wrap gap-2">
{image.categories.map((cat) => (
<Link
key={cat.id}
href={`/categories/${cat.id}`}
className="bg-muted text-sm px-2 py-1 rounded hover:bg-accent"
>
{cat.name}
</Link>
))}
</div>
</div>
)}
{/* Tags */}
{image.tags?.length > 0 && (
<div>
<h2 className="text-lg font-semibold mb-1">Tags</h2>
<div className="flex flex-wrap gap-2">
{image.tags.map((tag) => (
<Link
key={tag.id}
href={`/tags/${tag.id}`}
className="bg-muted text-sm px-2 py-1 rounded hover:bg-accent"
>
#{tag.name}
</Link>
))}
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,57 @@
// components/images/ImageMetadataBox.tsx
"use client"
import { Album, Category, Tag } from "@/generated/prisma"
import { FolderIcon, LayersIcon, TagIcon } from "lucide-react"
import Link from "next/link"
type Props = {
album: Album | null
categories: Category[]
tags: Tag[]
}
export default function ImageMetadataBox({ album, categories, tags }: Props) {
return (
<div className="border rounded-lg p-4 shadow bg-muted/20 w-full max-w-xl space-y-3">
{album && (
<div className="flex items-center gap-2">
<FolderIcon className="w-5 h-5 text-muted-foreground" />
<Link href={`/galleries/${album.galleryId}/${album.slug}`} className="hover:underline">
{album.name}
</Link>
</div>
)}
{categories.length > 0 && (
<div className="flex items-center gap-2 flex-wrap">
<LayersIcon className="w-5 h-5 text-muted-foreground" />
{categories.map((cat) => (
<Link
key={cat.id}
href={`/categories/${cat.id}`}
className="text-sm text-muted-foreground hover:underline"
>
{cat.name}
</Link>
))}
</div>
)}
{tags.length > 0 && (
<div className="flex items-center gap-2 flex-wrap">
<TagIcon className="w-5 h-5 text-muted-foreground" />
{tags.map((tag) => (
<Link
key={tag.id}
href={`/tags/${tag.id}`}
className="text-sm text-muted-foreground hover:underline"
>
{tag.name}
</Link>
))}
</div>
)}
</div>
)
}