diff --git a/public/images/Intersex-inclusive_pride_flag.png b/public/images/Intersex-inclusive_pride_flag.png new file mode 100644 index 0000000..a6663be Binary files /dev/null and b/public/images/Intersex-inclusive_pride_flag.png differ diff --git a/public/images/signal-username-qr-code.png b/public/images/signal-username-qr-code.png new file mode 100644 index 0000000..84d5b56 Binary files /dev/null and b/public/images/signal-username-qr-code.png differ diff --git a/src/app/(normal)/about/page.tsx b/src/app/(normal)/about/page.tsx new file mode 100644 index 0000000..ccf89da --- /dev/null +++ b/src/app/(normal)/about/page.tsx @@ -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 ( +
+
+

About

+

+ Fellies is a network of services for a small group of fluffy furry and furry-friendly folks.
+ We are here to support the furry and queer communities and provide a safe space for them to express themselves.
+ 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. +

+

+ This place is free of any AI or tracking functionality and is not intended to be used for any commercial purposes.
+ The usage of any content on this page for AI training is strictly prohibited. +

+
+ +
+

Connect with me

+
+ } href="mailto:core@fellies.email" label="Email" /> + } href="https://signal.me/#eu/ax5Sf75Na5g6c_Cir3Q9c0zv6TnoaQCKAo3dcUo15990aLTlbIBO1qbKqoB7WPuQ" label="Signal" /> + } href="https://matrix.to/#/@citali:steffo.dev" label="Matrix" /> + } href="https://fellies.social/@Citali" label="Fediverse" /> +
+
+ +
+
+ Inclusive Pride Flag +
+
+
+ ); +} + +function SocialLink({ icon, href, label }: { icon: React.ReactNode, href: string, label: string }) { + return ( + + {icon} + {label} + + ) +} \ No newline at end of file diff --git a/src/app/(normal)/artists/[artistSlug]/page.tsx b/src/app/(normal)/artists/[artistSlug]/page.tsx index 01687c6..59e9674 100644 --- a/src/app/(normal)/artists/[artistSlug]/page.tsx +++ b/src/app/(normal)/artists/[artistSlug]/page.tsx @@ -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 } } + }, + orderBy: [{ sortIndex: 'asc' }, { imageName: 'asc' }] }, - socials: { where: { isVisible: true }, orderBy: { isPrimary: "desc" } } + socials: { where: { isVisible: true }, orderBy: [{ sortIndex: 'asc' }, { isPrimary: "desc" }] } } }) @@ -33,8 +35,7 @@ export default async function ArtistPage({ params }: { params: { artistSlug: str

-

Images

- + ); } \ No newline at end of file diff --git a/src/app/(normal)/categories/[id]/page.tsx b/src/app/(normal)/categories/[id]/page.tsx index 0285b6d..b4d2789 100644 --- a/src/app/(normal)/categories/[id]/page.tsx +++ b/src/app/(normal)/categories/[id]/page.tsx @@ -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 ( -
-

Category not found

-
- ) + throw new Error("Category not found") } return ( -
- {category && ( - <> -
-

Category: {category.name}

-

- {category.description} -

-
- - - )} +
+
+

Category: {category.name}

+

+ {category.description} +

+
+
); } \ No newline at end of file diff --git a/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/[imageId]/page.tsx b/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/[imageId]/page.tsx index 174597a..2d0bbfc 100644 --- a/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/[imageId]/page.tsx +++ b/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/[imageId]/page.tsx @@ -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
Image not found
- const resizedVariant = image.variants.find(v => v.type === "resized"); - return (
- {image && ( -
-
-

{image.imageName}

-
- {resizedVariant && - - } -
- {image.artist && } - -
-

Image Metadata

- -

Name: {image.imageName}

- {image.altText &&

Alt Text: {image.altText}

} - {image.description &&

Description: {image.description}

} - {/*

NSFW: {image.nsfw ? "Yes" : "No"}

-

Upload Date: {new Date(image.uploadDate).toLocaleDateString()}

- {image.source &&

Source: {image.source}

} - {image.fileType &&

File Type: {image.fileType}

} - {image.fileSize &&

Size: {(image.fileSize / 1024).toFixed(1)} KB

} - {image.creationDate && ( -

Created: {new Date(image.creationDate).toLocaleDateString()}

- )} - {image.creationYear && image.creationMonth && ( -

Creation: {image.creationMonth}/{image.creationYear}

- )} */} - - {/* {image.metadata && ( - <> -

Metadata

-

Width: {image.metadata.width}px

-

Height: {image.metadata.height}px

-

Format: {image.metadata.format}

-

Color Space: {image.metadata.space}

-

Channels: {image.metadata.channels}

- {image.metadata.bitsPerSample && ( -

Bits Per Sample: {image.metadata.bitsPerSample}

- )} - {image.metadata.hasAlpha !== null && ( -

Has Alpha: {image.metadata.hasAlpha ? "Yes" : "No"}

- )} - - )} */} - - {/* {image.stats && ( - <> -

Statistics

-

Entropy: {image.stats.entropy.toFixed(2)}

-

Sharpness: {image.stats.sharpness.toFixed(2)}

-

Dominant Color: rgb({image.stats.dominantR}, {image.stats.dominantG}, {image.stats.dominantB})

-

Is Opaque: {image.stats.isOpaque ? "Yes" : "No"}

- - )} */} -
- - -
+
+
+

{image.imageName}

- )} + +
+ {image.artist && } + +
+
); } \ No newline at end of file diff --git a/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/page.tsx b/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/page.tsx index 49c7eff..7921263 100644 --- a/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/page.tsx +++ b/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/page.tsx @@ -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}

- + )}
diff --git a/src/app/(normal)/galleries/[gallerySlug]/page.tsx b/src/app/(normal)/galleries/[gallerySlug]/page.tsx index 374ad1d..c946304 100644 --- a/src/app/(normal)/galleries/[gallerySlug]/page.tsx +++ b/src/app/(normal)/galleries/[gallerySlug]/page.tsx @@ -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' } + } } }) diff --git a/src/app/(normal)/tags/[id]/page.tsx b/src/app/(normal)/tags/[id]/page.tsx new file mode 100644 index 0000000..5382ade --- /dev/null +++ b/src/app/(normal)/tags/[id]/page.tsx @@ -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 ( +
+
+

Tag: {tag.name}

+

+ {tag.description} +

+
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/cards/GlowingBorderWrapper.tsx b/src/components/cards/GlowingBorderWrapper.tsx index b28c081..ac3543c 100644 --- a/src/components/cards/GlowingBorderWrapper.tsx +++ b/src/components/cards/GlowingBorderWrapper.tsx @@ -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 (
-
{children}
+
{children}
) } diff --git a/src/components/cards/ImageCard.tsx b/src/components/cards/ImageCard.tsx index 1eb595b..8fc3d3e 100644 --- a/src/components/cards/ImageCard.tsx +++ b/src/components/cards/ImageCard.tsx @@ -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 = (
-
+
{image.nsfw && ( @@ -49,19 +72,29 @@ export default function ImageCard({ image, gallerySlug, albumSlug }: ImageCardPr
)}
-
-

{image.imageName}

-
+ {!isLarge && ( +
+

{image.imageName}

+
+ )}
) - return ( + + return disableLink ? ( + animateGlow && borderColors.length > 0 ? ( + {imageElement} + ) : ( + imageElement + ) + ) : ( {animateGlow && borderColors.length > 0 ? ( - {content} + {imageElement} ) : ( - content + imageElement )} ) } + diff --git a/src/components/images/ArtistInfoBox.tsx b/src/components/images/ArtistInfoBox.tsx index 68e496e..5f03510 100644 --- a/src/components/images/ArtistInfoBox.tsx +++ b/src/components/images/ArtistInfoBox.tsx @@ -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 (
-
- - +
+ {artist.displayName}
@@ -34,9 +31,6 @@ export default function ArtistInfoBox({ artist }: { artist: ArtistWithItems }) { > {social.platform}: {social.handle} - {social.isPrimary && ( - (main) - )}
) diff --git a/src/components/images/ImageMetadataBox.tsx b/src/components/images/ImageMetadataBox.tsx index 2a250e2..6b0488b 100644 --- a/src/components/images/ImageMetadataBox.tsx +++ b/src/components/images/ImageMetadataBox.tsx @@ -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 (
- {album && ( + {altText && (
- - + + {altText} +
+ )} + + {description && ( +
+ + {description} +
+ )} + + {creationLabel && ( +
+ + {creationLabel} +
+ )} + + {album && ( +
+ + {album.name}
@@ -25,12 +69,12 @@ export default function ImageMetadataBox({ album, categories, tags }: Props) { {categories.length > 0 && (
- + {categories.map((cat) => ( {cat.name} @@ -40,12 +84,12 @@ export default function ImageMetadataBox({ album, categories, tags }: Props) { {tags.length > 0 && (
- + {tags.map((tag) => ( {tag.name} diff --git a/src/components/lists/AlbumImageList.tsx b/src/components/lists/AlbumImageList.tsx new file mode 100644 index 0000000..b0f291e --- /dev/null +++ b/src/components/lists/AlbumImageList.tsx @@ -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

There are no images here!

; + + return ( +
+
+ {images.map((image) => ( + + ))} +
+
+ ); +} diff --git a/src/components/lists/GalleryList.tsx b/src/components/lists/GalleryList.tsx index 61c1716..d9d317e 100644 --- a/src/components/lists/GalleryList.tsx +++ b/src/components/lists/GalleryList.tsx @@ -12,6 +12,7 @@ export default async function GalleryList() { }, }, }, + orderBy: { sortIndex: 'asc' } }); if (!galleries.length) return

There are no galleries here!

; diff --git a/src/components/lists/ImageList.tsx b/src/components/lists/ImageList.tsx index 5eeb3ed..a39ccad 100644 --- a/src/components/lists/ImageList.tsx +++ b/src/components/lists/ImageList.tsx @@ -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

There are no images here!

; return (
- {images.map((image) => ( - - ))} + {images.map((image) => { + const gallerySlug = image.album?.gallery?.slug ? image.album.gallery.slug : '' + const albumSlug = image.album?.slug ? image.album.slug : '' + return () + })}
);