Basic layout finished
This commit is contained in:
48
src/components/images/ArtistInfoBox.tsx
Normal file
48
src/components/images/ArtistInfoBox.tsx
Normal 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>
|
||||
)
|
||||
}
|
76
src/components/images/GlowingImageBorder.tsx
Normal file
76
src/components/images/GlowingImageBorder.tsx
Normal 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>
|
||||
)
|
||||
}
|
37
src/components/images/GlowingImageWithToggle.tsx
Normal file
37
src/components/images/GlowingImageWithToggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
100
src/components/images/ImageInfoPanel.tsx
Normal file
100
src/components/images/ImageInfoPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
57
src/components/images/ImageMetadataBox.tsx
Normal file
57
src/components/images/ImageMetadataBox.tsx
Normal 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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user