Refine image page, add about page
This commit is contained in:
BIN
public/images/Intersex-inclusive_pride_flag.png
Normal file
BIN
public/images/Intersex-inclusive_pride_flag.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 105 KiB |
BIN
public/images/signal-username-qr-code.png
Normal file
BIN
public/images/signal-username-qr-code.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 190 KiB |
58
src/app/(normal)/about/page.tsx
Normal file
58
src/app/(normal)/about/page.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { MailIcon, MessageCircleIcon, RadioIcon, WavesIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-12 flex flex-col gap-10">
|
||||
<section className="text-center space-y-4">
|
||||
<h1 className="text-4xl font-bold tracking-tight">About</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Fellies is a network of services for a small group of fluffy furry and furry-friendly folks.<br />
|
||||
We are here to support the furry and queer communities and provide a safe space for them to express themselves.<br />
|
||||
This place is my own space to present all the nice artworks my (currently) three OCs including my main fursona Citali I've got from all the great and talented artists.
|
||||
</p>
|
||||
<p className="text-muted-foreground py-4">
|
||||
This place is free of any AI or tracking functionality and is not intended to be used for any commercial purposes.<br />
|
||||
The usage of any content on this page for AI training is strictly prohibited.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="border rounded-lg p-6 shadow bg-muted/20 space-y-4">
|
||||
<h2 className="text-xl font-semibold text-center">Connect with me</h2>
|
||||
<div className="flex flex-wrap gap-4 justify-between">
|
||||
<SocialLink icon={<MailIcon className="w-5 h-5" />} href="mailto:core@fellies.email" label="Email" />
|
||||
<SocialLink icon={<MessageCircleIcon className="w-5 h-5" />} href="https://signal.me/#eu/ax5Sf75Na5g6c_Cir3Q9c0zv6TnoaQCKAo3dcUo15990aLTlbIBO1qbKqoB7WPuQ" label="Signal" />
|
||||
<SocialLink icon={<WavesIcon className="w-5 h-5" />} href="https://matrix.to/#/@citali:steffo.dev" label="Matrix" />
|
||||
<SocialLink icon={<RadioIcon className="w-5 h-5" />} href="https://fellies.social/@Citali" label="Fediverse" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border rounded-lg p-6 shadow bg-muted/20 space-y-4">
|
||||
<div className="py-6 flex justify-center">
|
||||
<Image
|
||||
src="/images/Intersex-inclusive_pride_flag.png"
|
||||
alt="Inclusive Pride Flag"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded shadow"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SocialLink({ icon, href, label }: { icon: React.ReactNode, href: string, label: string }) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md bg-muted/30 hover:bg-muted/50 transition text-sm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import ArtistImageGrid from "@/components/artists/ArtistImageGrid";
|
||||
import ArtistInfoBox from "@/components/artists/ArtistInfoBox";
|
||||
import ImageList from "@/components/lists/ImageList";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function ArtistPage({ params }: { params: { artistSlug: string } }) {
|
||||
@ -13,10 +13,12 @@ export default async function ArtistPage({ params }: { params: { artistSlug: str
|
||||
images: {
|
||||
include: {
|
||||
variants: true,
|
||||
album: { include: { gallery: true } }
|
||||
}
|
||||
album: { include: { gallery: true } },
|
||||
colors: { include: { color: true } }
|
||||
},
|
||||
socials: { where: { isVisible: true }, orderBy: { isPrimary: "desc" } }
|
||||
orderBy: [{ sortIndex: 'asc' }, { imageName: 'asc' }]
|
||||
},
|
||||
socials: { where: { isVisible: true }, orderBy: [{ sortIndex: 'asc' }, { isPrimary: "desc" }] }
|
||||
}
|
||||
})
|
||||
|
||||
@ -33,8 +35,7 @@ export default async function ArtistPage({ params }: { params: { artistSlug: str
|
||||
</p>
|
||||
</section>
|
||||
<ArtistInfoBox artist={artist} />
|
||||
<h2 className="text-2xl font-bold tracking-tight">Images</h2>
|
||||
<ArtistImageGrid images={artist.images} />
|
||||
<ImageList images={artist.images} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import CategoryImageList from "@/components/categories/CategoryImageList";
|
||||
import ImageList from "@/components/lists/ImageList";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function CategoriesSinglePage({ params }: { params: { id: string } }) {
|
||||
@ -10,48 +10,29 @@ export default async function CategoriesSinglePage({ params }: { params: { id: s
|
||||
},
|
||||
include: {
|
||||
images: {
|
||||
select: {
|
||||
id: true,
|
||||
imageName: true,
|
||||
fileKey: true,
|
||||
nsfw: true,
|
||||
altText: true,
|
||||
album: {
|
||||
select: {
|
||||
slug: true,
|
||||
gallery: {
|
||||
select: {
|
||||
slug: true,
|
||||
include: {
|
||||
variants: true,
|
||||
album: { include: { gallery: true } },
|
||||
colors: { include: { color: true } }
|
||||
},
|
||||
orderBy: [{ sortIndex: 'asc' }, { imageName: 'asc' }]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-12 space-y-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight">Category not found</h1>
|
||||
</div>
|
||||
)
|
||||
throw new Error("Category not found")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-12 space-y-12">
|
||||
{category && (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto px-4 py-12 space-y-12 flex flex-col items-center">
|
||||
<section className="text-center space-y-4">
|
||||
<h1 className="text-4xl font-bold tracking-tight">Category: {category.name}</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{category.description}
|
||||
</p>
|
||||
</section>
|
||||
<CategoryImageList images={category.images} />
|
||||
</>
|
||||
)}
|
||||
<ImageList images={category.images} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import ImageCard from "@/components/cards/ImageCard";
|
||||
import ArtistInfoBox from "@/components/images/ArtistInfoBox";
|
||||
import GlowingImageWithToggle from "@/components/images/GlowingImageWithToggle";
|
||||
import ImageMetadataBox from "@/components/images/ImageMetadataBox";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
@ -11,11 +11,11 @@ export default async function ImagePage({ params }: { params: { gallerySlug: str
|
||||
id: imageId
|
||||
},
|
||||
include: {
|
||||
artist: { include: { socials: true } },
|
||||
artist: {
|
||||
include: { socials: { where: { isVisible: true }, orderBy: [{ sortIndex: 'asc' }, { isPrimary: "desc" }] } }
|
||||
},
|
||||
colors: { include: { color: true } },
|
||||
extractColors: { include: { extract: true } },
|
||||
palettes: { include: { palette: { include: { items: true } } } },
|
||||
album: true,
|
||||
album: { include: { gallery: true } },
|
||||
categories: true,
|
||||
tags: true,
|
||||
variants: true,
|
||||
@ -26,82 +26,27 @@ export default async function ImagePage({ params }: { params: { gallerySlug: str
|
||||
|
||||
if (!image) return <div>Image not found</div>
|
||||
|
||||
const resizedVariant = image.variants.find(v => v.type === "resized");
|
||||
|
||||
return (
|
||||
<div>
|
||||
{image && (
|
||||
<div className="flex flex-col items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4 pb-8">{image.imageName}</h1>
|
||||
</div>
|
||||
{resizedVariant &&
|
||||
<GlowingImageWithToggle
|
||||
alt={image.imageName}
|
||||
variant={resizedVariant}
|
||||
colors={image.colors}
|
||||
src={`/api/image/${resizedVariant.s3Key}`}
|
||||
nsfw={image.nsfw}
|
||||
imageId={image.id}
|
||||
/>
|
||||
}
|
||||
<ImageCard image={image} gallerySlug={image.album?.gallery?.slug} albumSlug={image.album?.slug} size="large" />
|
||||
<section className="py-8 flex flex-col gap-4">
|
||||
{image.artist && <ArtistInfoBox artist={image.artist} />}
|
||||
|
||||
<div className="border rounded-lg p-4 shadow bg-muted/30 w-full max-w-xl">
|
||||
<h2 className="text-xl font-semibold mb-2">Image Metadata</h2>
|
||||
|
||||
<p><strong>Name:</strong> {image.imageName}</p>
|
||||
{image.altText && <p><strong>Alt Text:</strong> {image.altText}</p>}
|
||||
{image.description && <p><strong>Description:</strong> {image.description}</p>}
|
||||
{/* <p><strong>NSFW:</strong> {image.nsfw ? "Yes" : "No"}</p>
|
||||
<p><strong>Upload Date:</strong> {new Date(image.uploadDate).toLocaleDateString()}</p>
|
||||
{image.source && <p><strong>Source:</strong> {image.source}</p>}
|
||||
{image.fileType && <p><strong>File Type:</strong> {image.fileType}</p>}
|
||||
{image.fileSize && <p><strong>Size:</strong> {(image.fileSize / 1024).toFixed(1)} KB</p>}
|
||||
{image.creationDate && (
|
||||
<p><strong>Created:</strong> {new Date(image.creationDate).toLocaleDateString()}</p>
|
||||
)}
|
||||
{image.creationYear && image.creationMonth && (
|
||||
<p><strong>Creation:</strong> {image.creationMonth}/{image.creationYear}</p>
|
||||
)} */}
|
||||
|
||||
{/* {image.metadata && (
|
||||
<>
|
||||
<h3 className="text-lg font-medium mt-4">Metadata</h3>
|
||||
<p><strong>Width:</strong> {image.metadata.width}px</p>
|
||||
<p><strong>Height:</strong> {image.metadata.height}px</p>
|
||||
<p><strong>Format:</strong> {image.metadata.format}</p>
|
||||
<p><strong>Color Space:</strong> {image.metadata.space}</p>
|
||||
<p><strong>Channels:</strong> {image.metadata.channels}</p>
|
||||
{image.metadata.bitsPerSample && (
|
||||
<p><strong>Bits Per Sample:</strong> {image.metadata.bitsPerSample}</p>
|
||||
)}
|
||||
{image.metadata.hasAlpha !== null && (
|
||||
<p><strong>Has Alpha:</strong> {image.metadata.hasAlpha ? "Yes" : "No"}</p>
|
||||
)}
|
||||
</>
|
||||
)} */}
|
||||
|
||||
{/* {image.stats && (
|
||||
<>
|
||||
<h3 className="text-lg font-medium mt-4">Statistics</h3>
|
||||
<p><strong>Entropy:</strong> {image.stats.entropy.toFixed(2)}</p>
|
||||
<p><strong>Sharpness:</strong> {image.stats.sharpness.toFixed(2)}</p>
|
||||
<p><strong>Dominant Color:</strong> rgb({image.stats.dominantR}, {image.stats.dominantG}, {image.stats.dominantB})</p>
|
||||
<p><strong>Is Opaque:</strong> {image.stats.isOpaque ? "Yes" : "No"}</p>
|
||||
</>
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
<ImageMetadataBox
|
||||
album={image.album}
|
||||
categories={image.categories}
|
||||
tags={image.tags}
|
||||
creationDate={image.creationDate}
|
||||
creationYear={image.creationYear}
|
||||
creationMonth={image.creationMonth}
|
||||
altText={image.altText}
|
||||
description={image.description}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div >
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import ImageList from "@/components/lists/ImageList";
|
||||
import AlbumImageList from "@/components/lists/AlbumImageList";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function GalleryPage({ params }: { params: { gallerySlug: string, albumSlug: string } }) {
|
||||
@ -21,7 +21,10 @@ export default async function GalleryPage({ params }: { params: { gallerySlug: s
|
||||
},
|
||||
},
|
||||
include: {
|
||||
images: { include: { colors: { include: { color: true } } } },
|
||||
images: {
|
||||
include: { colors: { include: { color: true } } },
|
||||
orderBy: [{ sortIndex: 'asc' }, { imageName: 'asc' }]
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -35,7 +38,7 @@ export default async function GalleryPage({ params }: { params: { gallerySlug: s
|
||||
{album.description}
|
||||
</p>
|
||||
</section>
|
||||
<ImageList images={album.images} gallerySlug={gallerySlug} albumSlug={albumSlug} />
|
||||
<AlbumImageList images={album.images} gallerySlug={gallerySlug} albumSlug={albumSlug} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -9,7 +9,11 @@ export default async function GalleryPage({ params }: { params: { gallerySlug: s
|
||||
slug: gallerySlug
|
||||
},
|
||||
include: {
|
||||
albums: { where: { images: { some: {} } }, include: { coverImage: { include: { colors: { include: { color: true } } } } } }
|
||||
albums: {
|
||||
where: { images: { some: {} } },
|
||||
include: { coverImage: { include: { colors: { include: { color: true } } } } },
|
||||
orderBy: { sortIndex: 'asc' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
38
src/app/(normal)/tags/[id]/page.tsx
Normal file
38
src/app/(normal)/tags/[id]/page.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import ImageList from "@/components/lists/ImageList";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function TagsSinglePage({ params }: { params: { id: string } }) {
|
||||
const { id } = await params;
|
||||
|
||||
const tag = await prisma.tag.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
images: {
|
||||
include: {
|
||||
variants: true,
|
||||
album: { include: { gallery: true } },
|
||||
colors: { include: { color: true } }
|
||||
},
|
||||
orderBy: [{ sortIndex: 'asc' }, { imageName: 'asc' }]
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
if (!tag) {
|
||||
throw new Error("Tag not found")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-12 space-y-12 flex flex-col items-center">
|
||||
<section className="text-center space-y-4">
|
||||
<h1 className="text-4xl font-bold tracking-tight">Tag: {tag.name}</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{tag.description}
|
||||
</p>
|
||||
</section>
|
||||
<ImageList images={tag.images} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -9,6 +9,7 @@ interface GlowingBorderWrapperProps {
|
||||
className?: string
|
||||
animate?: boolean
|
||||
colors?: string[]
|
||||
size?: "default" | "large"
|
||||
}
|
||||
|
||||
export function GlowingBorderWrapper({
|
||||
@ -16,6 +17,7 @@ export function GlowingBorderWrapper({
|
||||
className,
|
||||
animate = true,
|
||||
colors = [],
|
||||
size = "default"
|
||||
}: GlowingBorderWrapperProps) {
|
||||
const { theme } = useTheme()
|
||||
|
||||
@ -25,10 +27,16 @@ export function GlowingBorderWrapper({
|
||||
? "rgba(255,255,255,0.2), rgba(255,255,255,0.05)"
|
||||
: "rgba(0,0,0,0.1), rgba(0,0,0,0.03)"
|
||||
|
||||
const padding = size === "large" ? "p-[6px]" : "p-[2px]"
|
||||
const roundedOuter = size === "large" ? "rounded-xl" : "rounded-lg"
|
||||
const roundedInner = size === "large" ? "rounded-lg" : "rounded-md"
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-lg p-[2px]",
|
||||
"relative",
|
||||
padding,
|
||||
roundedOuter,
|
||||
animate && "animate-glow",
|
||||
className
|
||||
)}
|
||||
@ -36,7 +44,7 @@ export function GlowingBorderWrapper({
|
||||
background: `linear-gradient(135deg, ${gradientColors})`,
|
||||
}}
|
||||
>
|
||||
<div className="rounded-md overflow-hidden">{children}</div>
|
||||
<div className={cn("overflow-hidden", roundedInner)}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { type Color, type Image, type ImageColor } from "@/generated/prisma"
|
||||
import { ImageVariant, type Color, type Image, type ImageColor } from "@/generated/prisma"
|
||||
import { useGlobalSettings } from "@/hooks/useGlobalSettings"
|
||||
import clsx from "clsx"
|
||||
import { EyeOffIcon } from "lucide-react"
|
||||
@ -10,32 +10,55 @@ import { GlowingBorderWrapper } from "./GlowingBorderWrapper"
|
||||
import { TagBadge } from "./TagBadge"
|
||||
|
||||
type ImageWithColors = Image & {
|
||||
colors?: (ImageColor & { color: Color })[]
|
||||
colors?: (ImageColor & { color: Color })[],
|
||||
variants?: ImageVariant[]
|
||||
}
|
||||
|
||||
type ImageCardProps = {
|
||||
image: ImageWithColors,
|
||||
gallerySlug?: string
|
||||
albumSlug?: string
|
||||
image: ImageWithColors;
|
||||
gallerySlug?: string;
|
||||
albumSlug?: string;
|
||||
size?: "default" | "large";
|
||||
disableLink?: boolean;
|
||||
}
|
||||
|
||||
export default function ImageCard({ image, gallerySlug, albumSlug }: ImageCardProps) {
|
||||
export default function ImageCard({
|
||||
image,
|
||||
gallerySlug,
|
||||
albumSlug,
|
||||
size = "default",
|
||||
disableLink = false,
|
||||
}: ImageCardProps) {
|
||||
const { showNSFW, animateGlow } = useGlobalSettings()
|
||||
const shouldBlur = image.nsfw && !showNSFW
|
||||
|
||||
const href = `/galleries/${gallerySlug}/${albumSlug}/${image.id}`
|
||||
const isLarge = size === "large"
|
||||
const href = isLarge
|
||||
? `/raw/${image.id}`
|
||||
: `/galleries/${gallerySlug}/${albumSlug}/${image.id}`
|
||||
|
||||
const borderColors =
|
||||
image.colors?.map((c) => c.color?.hex).filter((hex): hex is string => Boolean(hex)) ?? []
|
||||
|
||||
const shouldBlur = image.nsfw && !showNSFW
|
||||
const thumbnailSrc = `/api/image/thumbnails/${image.fileKey}.webp`
|
||||
const resizedVariant = image.variants?.find((v) => v.type === "resized")
|
||||
const resizedSrc = resizedVariant ? `/api/image/${resizedVariant.s3Key}` : undefined
|
||||
|
||||
const content = (
|
||||
const imageSrc = isLarge && resizedSrc ? resizedSrc : thumbnailSrc
|
||||
const width = isLarge && resizedVariant?.width ? resizedVariant.width : undefined
|
||||
const height = isLarge && resizedVariant?.height ? resizedVariant.height : undefined
|
||||
|
||||
const imageElement = (
|
||||
<div className="group rounded-lg border overflow-hidden hover:shadow-lg transition-shadow bg-background relative">
|
||||
<div className="relative aspect-[4/3] w-full bg-muted items-center justify-center">
|
||||
<div className="relative w-full bg-muted items-center justify-center"
|
||||
style={isLarge && width && height ? { width, height } : { aspectRatio: "4 / 3" }}
|
||||
>
|
||||
<NextImage
|
||||
src={`/api/image/thumbnails/${image.fileKey}.webp`}
|
||||
src={imageSrc}
|
||||
alt={image.imageName}
|
||||
fill
|
||||
fill={!width || !height}
|
||||
width={width}
|
||||
height={height}
|
||||
className={clsx("object-cover transition duration-300", shouldBlur && "blur-md scale-105")}
|
||||
/>
|
||||
{image.nsfw && (
|
||||
@ -49,19 +72,29 @@ export default function ImageCard({ image, gallerySlug, albumSlug }: ImageCardPr
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isLarge && (
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-semibold truncate text-center">{image.imageName}</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
return disableLink ? (
|
||||
animateGlow && borderColors.length > 0 ? (
|
||||
<GlowingBorderWrapper colors={borderColors} size={size}>{imageElement}</GlowingBorderWrapper>
|
||||
) : (
|
||||
imageElement
|
||||
)
|
||||
) : (
|
||||
<Link href={href}>
|
||||
{animateGlow && borderColors.length > 0 ? (
|
||||
<GlowingBorderWrapper colors={borderColors}>{content}</GlowingBorderWrapper>
|
||||
<GlowingBorderWrapper colors={borderColors} size={size}>{imageElement}</GlowingBorderWrapper>
|
||||
) : (
|
||||
content
|
||||
imageElement
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
// 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 & {
|
||||
@ -13,9 +11,8 @@ type ArtistWithItems = Artist & {
|
||||
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">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Link href={`/artists/${artist.slug}`} className="font-semibold text-xl hover:underline">
|
||||
{artist.displayName}
|
||||
</Link>
|
||||
</div>
|
||||
@ -34,9 +31,6 @@ export default function ArtistInfoBox({ artist }: { artist: ArtistWithItems }) {
|
||||
>
|
||||
<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>
|
||||
)
|
||||
|
@ -1,23 +1,67 @@
|
||||
// components/images/ImageMetadataBox.tsx
|
||||
"use client"
|
||||
|
||||
import { Album, Category, Tag } from "@/generated/prisma"
|
||||
import { FolderIcon, LayersIcon, TagIcon } from "lucide-react"
|
||||
import { CalendarDaysIcon, FolderIcon, LayersIcon, QuoteIcon, TagIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
type Props = {
|
||||
album: Album | null
|
||||
categories: Category[]
|
||||
tags: Tag[]
|
||||
creationDate?: Date | null
|
||||
creationYear?: number | null
|
||||
creationMonth?: number | null
|
||||
altText?: string | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
export default function ImageMetadataBox({ album, categories, tags }: Props) {
|
||||
export default function ImageMetadataBox({
|
||||
album,
|
||||
categories,
|
||||
tags,
|
||||
creationDate,
|
||||
creationYear,
|
||||
creationMonth,
|
||||
altText,
|
||||
description
|
||||
}: Props) {
|
||||
const creationLabel = creationDate
|
||||
? new Date(creationDate).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: creationYear && creationMonth
|
||||
? `${creationMonth.toString().padStart(2, "0")}/${creationYear}`
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg p-4 shadow bg-muted/20 w-full max-w-xl space-y-3">
|
||||
{album && (
|
||||
{altText && (
|
||||
<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">
|
||||
<TagIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
||||
<span className="text-sm text-muted-foreground">{altText}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{description && (
|
||||
<div className="flex items-center gap-2">
|
||||
<QuoteIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
||||
<span className="text-sm text-muted-foreground">{description}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{creationLabel && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarDaysIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
||||
<span className="text-sm text-muted-foreground">{creationLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{album && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<FolderIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
||||
<Link href={`/galleries/${album.galleryId}/${album.slug}`} className="text-sm underline">
|
||||
{album.name}
|
||||
</Link>
|
||||
</div>
|
||||
@ -25,12 +69,12 @@ export default function ImageMetadataBox({ album, categories, tags }: Props) {
|
||||
|
||||
{categories.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<LayersIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<LayersIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
||||
{categories.map((cat) => (
|
||||
<Link
|
||||
key={cat.id}
|
||||
href={`/categories/${cat.id}`}
|
||||
className="text-sm text-muted-foreground hover:underline"
|
||||
className="text-sm underline"
|
||||
>
|
||||
{cat.name}
|
||||
</Link>
|
||||
@ -40,12 +84,12 @@ export default function ImageMetadataBox({ album, categories, tags }: Props) {
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<TagIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<TagIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
||||
{tags.map((tag) => (
|
||||
<Link
|
||||
key={tag.id}
|
||||
href={`/tags/${tag.id}`}
|
||||
className="text-sm text-muted-foreground hover:underline"
|
||||
className="text-sm underline"
|
||||
>
|
||||
{tag.name}
|
||||
</Link>
|
||||
|
16
src/components/lists/AlbumImageList.tsx
Normal file
16
src/components/lists/AlbumImageList.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { Image } from "@/generated/prisma";
|
||||
import ImageCard from "../cards/ImageCard";
|
||||
|
||||
export default function AlbumImageList({ images, gallerySlug, albumSlug }: { images: Image[], gallerySlug: string, albumSlug: string }) {
|
||||
if (!images.length) return <p>There are no images here!</p>;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{images.map((image) => (
|
||||
<ImageCard key={image.id} image={image} gallerySlug={gallerySlug} albumSlug={albumSlug} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
@ -12,6 +12,7 @@ export default async function GalleryList() {
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { sortIndex: 'asc' }
|
||||
});
|
||||
|
||||
if (!galleries.length) return <p>There are no galleries here!</p>;
|
||||
|
@ -1,15 +1,23 @@
|
||||
import { Image } from "@/generated/prisma";
|
||||
import { Album, Gallery, Image } from "@/generated/prisma";
|
||||
import ImageCard from "../cards/ImageCard";
|
||||
|
||||
export default function ImageList({ images, gallerySlug, albumSlug }: { images: Image[], gallerySlug: string, albumSlug: string }) {
|
||||
type ImageWithItems = Image & {
|
||||
album: Album & {
|
||||
gallery: Gallery | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export default function ImageList({ images }: { images: ImageWithItems[] }) {
|
||||
if (!images.length) return <p>There are no images here!</p>;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{images.map((image) => (
|
||||
<ImageCard key={image.id} image={image} gallerySlug={gallerySlug} albumSlug={albumSlug} />
|
||||
))}
|
||||
{images.map((image) => {
|
||||
const gallerySlug = image.album?.gallery?.slug ? image.album.gallery.slug : ''
|
||||
const albumSlug = image.album?.slug ? image.album.slug : ''
|
||||
return (<ImageCard key={image.id} image={image} gallerySlug={gallerySlug} albumSlug={albumSlug} />)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
Reference in New Issue
Block a user