Add minor UX improvements
Build and push Docker image / build-and-push (push) Successful in 3m46s

This commit is contained in:
2026-08-31 05:35:53 +02:00
parent b11366fb4e
commit 7daf2b1783
9 changed files with 547 additions and 241 deletions
+65 -38
View File
@@ -4,52 +4,79 @@ import Topbar from '@/components/Topbar';
import { ImagesResponse, TagsResponse } from '@/interfaces/api';
import { PaginatorPosition } from '@/interfaces/paginator';
import { redirect } from 'next/navigation';
import { Suspense } from 'react';
export default async function GalleryPage({ params }: { params: Promise<{ page: number; tag: string }> }) {
const { tag, page } = await params;
interface GalleryPageProps {
params: Promise<{
tag: string;
page: string;
}>;
}
const fetchImages = async () => {
const apiUrl = new URL(
`/api/images?page=${page}&tag=${tag === 'all' ? '' : tag}`,
process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000',
);
const response = await fetch(apiUrl.toString());
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
export default async function GalleryPage({
params,
}: GalleryPageProps) {
const { tag, page: pageParam } = await params;
const page = Number.parseInt(pageParam, 10);
return (await response.json()) as ImagesResponse;
};
const fetchTags = async () => {
const apiUrl = new URL('/api/tags', process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000');
const response = await fetch(apiUrl.toString());
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
return (await response.json()) as TagsResponse;
};
const images = await fetchImages();
const tags = await fetchTags();
if (images.totalPages < page && images.totalPages > 0) {
redirect(`/gallery/${tag}/${images.totalPages}`);
if (!Number.isInteger(page) || page < 1) {
redirect(`/gallery/${encodeURIComponent(tag)}/1`);
}
if (page <= 0) {
redirect(`/gallery/${tag}/1`);
const siteUrl =
process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000';
const imagesUrl = new URL('/api/images', siteUrl);
imagesUrl.searchParams.set('page', String(page));
imagesUrl.searchParams.set('tag', tag === 'all' ? '' : tag);
const tagsUrl = new URL('/api/tags', siteUrl);
const [imagesResponse, tagsResponse] = await Promise.all([
fetch(imagesUrl, {
cache: 'no-store',
}),
fetch(tagsUrl, {
cache: 'no-store',
}),
]);
if (!imagesResponse.ok) {
throw new Error(
`Failed to load images: ${imagesResponse.status} ${imagesResponse.statusText}`,
);
}
if (!tagsResponse.ok) {
throw new Error(
`Failed to load tags: ${tagsResponse.status} ${tagsResponse.statusText}`,
);
}
const images = (await imagesResponse.json()) as ImagesResponse;
const tags = (await tagsResponse.json()) as TagsResponse;
if (images.totalPages > 0 && page > images.totalPages) {
redirect(
`/gallery/${encodeURIComponent(tag)}/${images.totalPages}`,
);
}
return (
<>
<Suspense fallback={<div>Loading gallery...</div>}>
<Topbar activeTag={tag} tags={tags} page={page} totalPages={images.totalPages} />
<Gallery initialImages={images.images} />
<Paginator page={page} totalPages={images.totalPages} position={PaginatorPosition.BOTTOM} />
</Suspense>
<Topbar
activeTag={tag}
tags={tags}
page={page}
totalPages={images.totalPages}
/>
<Gallery initialImages={images.images} />
<Paginator
page={page}
totalPages={images.totalPages}
position={PaginatorPosition.BOTTOM}
/>
</>
);
}
}
+10 -2
View File
@@ -1,5 +1,13 @@
import { redirect } from 'next/navigation';
export default async function Page({ params }: { params: Promise<{ page: number; tag: string }> }) {
redirect(`gallery/${(await params).tag}/1`);
interface TagPageProps {
params: Promise<{
tag: string;
}>;
}
export default async function TagPage({ params }: TagPageProps) {
const { tag } = await params;
redirect(`/gallery/${encodeURIComponent(tag)}/1`);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { redirect } from 'next/navigation';
export default async function Page() {
redirect(`gallery/all/1`);
redirect(`/gallery/all/1`);
}
+25 -11
View File
@@ -1,6 +1,8 @@
* {
--header-height: 50px;
--color-accent: #3cdbc0;
--color-background: #0d1113;
--color-surface: #151b1d;
}
:root {
@@ -8,9 +10,9 @@
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color-scheme: dark;
color: rgba(255, 255, 255, 0.87);
background-color: rgb(27, 27, 27);
background-color: var(--color-background);
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -18,22 +20,34 @@
-moz-osx-font-smoothing: grayscale;
}
body,
#root {
html {
min-height: 100%;
background-color: var(--color-background);
}
body {
margin: 0;
padding: 0;
min-height: 100vh;
font-size: 16px;
letter-spacing: 1px;
scrollbar-gutter: stable;
background-color: var(--color-background);
background-image:
radial-gradient(circle at 85% 10%, rgba(60, 219, 192, 0.12) 0%, transparent 32%),
radial-gradient(circle at 10% 85%, rgba(27, 115, 124, 0.08) 0%, transparent 38%),
linear-gradient(145deg, #0d1113 0%, #101719 50%, #0b0e10 100%);
background-attachment: fixed;
}
#root {
display: flex;
flex-direction: column;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
letter-spacing: 1px;
min-height: 100vh;
scrollbar-gutter: stable;
background: linear-gradient(-120deg, rgba(0, 0, 0, 0), #06565a49);
}
a {
text-decoration: none;
color: inherit;
text-decoration: none;
}
+1
View File
@@ -4,6 +4,7 @@
padding: 0 40px;
overflow: hidden;
background-image: url('/landing-page.jpg');
min-height: calc(100vh - var(--header-height));
background-size: cover;
@media (max-width: 1200px) {