feat: polish app loading screen and home section emphasis
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m52s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m52s
Refine the global loading experience to feel smoother and less flashy while keeping brand accents. Simplify the home continue-reading highlight by styling the section header instead of using a heavy card wrapper.
This commit is contained in:
44
src/app/loading.tsx
Normal file
44
src/app/loading.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const SHOW_DELAY_MS = 140;
|
||||||
|
|
||||||
|
export default function AppLoading() {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
setIsVisible(true);
|
||||||
|
}, SHOW_DELAY_MS);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
className={`flex min-h-screen items-center justify-center px-6 transition-opacity duration-300 motion-reduce:transition-none ${
|
||||||
|
isVisible ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex w-full max-w-sm flex-col items-center gap-6 text-center">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="bg-gradient-to-r from-primary via-cyan-500 to-fuchsia-500 bg-clip-text text-xl font-bold uppercase tracking-[0.12em] text-transparent">
|
||||||
|
StripStream
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Chargement de votre bibliotheque...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2" aria-hidden>
|
||||||
|
<span className="h-2 w-2 animate-bounce rounded-full bg-primary [animation-delay:-200ms]" />
|
||||||
|
<span className="h-2 w-2 animate-bounce rounded-full bg-cyan-500 [animation-delay:-100ms]" />
|
||||||
|
<span className="h-2 w-2 animate-bounce rounded-full bg-fuchsia-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative h-1.5 w-56 overflow-hidden rounded-full bg-muted/80" aria-hidden>
|
||||||
|
<div className="animate-loader-slide absolute inset-y-0 w-1/3 rounded-full bg-gradient-to-r from-primary via-cyan-500 to-fuchsia-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -31,13 +31,12 @@ export function HomeContent({ data }: HomeContentProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-10 pb-2">
|
<div className="space-y-10 pb-2">
|
||||||
{data.ongoingBooks && data.ongoingBooks.length > 0 && (
|
{data.ongoingBooks && data.ongoingBooks.length > 0 && (
|
||||||
<div className="rounded-2xl border border-primary/20 bg-[linear-gradient(145deg,hsl(var(--primary)/0.12),hsl(var(--background)/0.1)_45%)] p-4 sm:p-5">
|
<MediaRow
|
||||||
<MediaRow
|
titleKey="home.sections.continue_reading"
|
||||||
titleKey="home.sections.continue_reading"
|
items={optimizeBookData(data.ongoingBooks)}
|
||||||
items={optimizeBookData(data.ongoingBooks)}
|
iconName="BookOpen"
|
||||||
iconName="BookOpen"
|
featuredHeader
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data.ongoing && data.ongoing.length > 0 && (
|
{data.ongoing && data.ongoing.length > 0 && (
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface MediaRowProps {
|
|||||||
titleKey: string;
|
titleKey: string;
|
||||||
items: (OptimizedSeries | OptimizedBook)[];
|
items: (OptimizedSeries | OptimizedBook)[];
|
||||||
iconName?: string;
|
iconName?: string;
|
||||||
|
featuredHeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
@@ -51,7 +52,7 @@ const iconMap = {
|
|||||||
History,
|
History,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MediaRow({ titleKey, items, iconName }: MediaRowProps) {
|
export function MediaRow({ titleKey, items, iconName, featuredHeader = false }: MediaRowProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const icon = iconName ? iconMap[iconName as keyof typeof iconMap] : undefined;
|
const icon = iconName ? iconMap[iconName as keyof typeof iconMap] : undefined;
|
||||||
@@ -68,7 +69,13 @@ export function MediaRow({ titleKey, items, iconName }: MediaRowProps) {
|
|||||||
title={t(titleKey)}
|
title={t(titleKey)}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className="space-y-5"
|
className="space-y-5"
|
||||||
headerClassName="border-b border-border/50 pb-2"
|
headerClassName={cn("border-b border-border/50 pb-2", featuredHeader && "border-primary/25")}
|
||||||
|
titleClassName={
|
||||||
|
featuredHeader
|
||||||
|
? "bg-gradient-to-r from-primary via-cyan-500 to-fuchsia-500 bg-clip-text text-transparent"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
iconClassName={featuredHeader ? "text-primary" : undefined}
|
||||||
>
|
>
|
||||||
<ScrollContainer
|
<ScrollContainer
|
||||||
showArrows={true}
|
showArrows={true}
|
||||||
|
|||||||
@@ -8,11 +8,24 @@ export interface SectionProps extends React.HTMLAttributes<HTMLElement> {
|
|||||||
description?: string;
|
description?: string;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
headerClassName?: string;
|
headerClassName?: string;
|
||||||
|
titleClassName?: string;
|
||||||
|
iconClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Section = React.forwardRef<HTMLElement, SectionProps>(
|
const Section = React.forwardRef<HTMLElement, SectionProps>(
|
||||||
(
|
(
|
||||||
{ title, icon: Icon, description, actions, children, className, headerClassName, ...props },
|
{
|
||||||
|
title,
|
||||||
|
icon: Icon,
|
||||||
|
description,
|
||||||
|
actions,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
headerClassName,
|
||||||
|
titleClassName,
|
||||||
|
iconClassName,
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
@@ -21,9 +34,9 @@ const Section = React.forwardRef<HTMLElement, SectionProps>(
|
|||||||
<div className={cn("flex items-center justify-between", headerClassName)}>
|
<div className={cn("flex items-center justify-between", headerClassName)}>
|
||||||
{title && (
|
{title && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{Icon && <Icon className="h-5 w-5 text-muted-foreground" />}
|
{Icon && <Icon className={cn("h-5 w-5 text-muted-foreground", iconClassName)} />}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
|
<h2 className={cn("text-2xl font-bold tracking-tight", titleClassName)}>{title}</h2>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -164,4 +164,17 @@ body.no-pinch-zoom * {
|
|||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fade-in 0.3s ease-in forwards;
|
animation: fade-in 0.3s ease-in forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes loader-slide {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-120%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(320%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-loader-slide {
|
||||||
|
animation: loader-slide 1.25s ease-in-out infinite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user