feat: simplify error handling in login and registration pages, add install prompt component, and update favicon and icons

This commit is contained in:
2025-09-07 18:43:56 +02:00
parent c6f5bab1eb
commit 5f8cf49ec9
20 changed files with 160 additions and 23 deletions

View File

@@ -42,7 +42,7 @@ export default function LoginPage() {
const { token } = await loginUser(values); const { token } = await loginUser(values);
setToken(token); setToken(token);
router.push("/"); // Redirect to homepage on successful login router.push("/"); // Redirect to homepage on successful login
} catch (err) { } catch {
setError("Invalid username or password."); setError("Invalid username or password.");
} }
} }

View File

@@ -40,7 +40,7 @@ export default function RegisterPage() {
await registerUser(values); await registerUser(values);
// You can automatically log the user in here or just redirect them // You can automatically log the user in here or just redirect them
router.push("/login"); router.push("/login");
} catch (err) { } catch {
setError("Username or email may already be taken."); setError("Username or email may already be taken.");
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -4,6 +4,7 @@ import { AuthProvider } from "@/hooks/use-auth";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import localFont from "next/font/local"; import localFont from "next/font/local";
import InstallPrompt from "@/components/install-prompt";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Thoughts", title: "Thoughts",
@@ -37,6 +38,7 @@ export default function RootLayout({
<AuthProvider> <AuthProvider>
<Header /> <Header />
<main className="flex-1">{children}</main> <main className="flex-1">{children}</main>
<InstallPrompt />
<Toaster /> <Toaster />
</AuthProvider> </AuthProvider>
</body> </body>

View File

@@ -0,0 +1,25 @@
import type { MetadataRoute } from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Thoughts',
short_name: 'Thoughts',
description: 'A social network for sharing thoughts',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#000000',
icons: [
{
src: '/icon-192x192.webp',
sizes: '192x192',
type: 'image/webp',
},
{
src: '/icon.avif',
sizes: '512x512',
type: 'image/avif',
},
],
}
}

View File

@@ -14,6 +14,7 @@ import { PopularTags } from "@/components/popular-tags";
import { ThoughtThread } from "@/components/thought-thread"; import { ThoughtThread } from "@/components/thought-thread";
import { buildThoughtThreads } from "@/lib/utils"; import { buildThoughtThreads } from "@/lib/utils";
import { TopFriends } from "@/components/top-friends"; import { TopFriends } from "@/components/top-friends";
import InstallPrompt from "@/components/install-prompt";
export default async function Home() { export default async function Home() {
const token = (await cookies()).get("auth_token")?.value ?? null; const token = (await cookies()).get("auth_token")?.value ?? null;
@@ -101,26 +102,28 @@ async function FeedPage({ token }: { token: string }) {
function LandingPage() { function LandingPage() {
return ( return (
<div className="font-sans min-h-screen text-gray-800 flex items-center justify-center"> <>
<div className="container mx-auto max-w-2xl p-4 sm:p-6 text-center glass-effect glossy-effect bottom rounded-md shadow-fa-lg"> <div className="font-sans min-h-screen text-gray-800 flex items-center justify-center">
<h1 <div className="container mx-auto max-w-2xl p-4 sm:p-6 text-center glass-effect glossy-effect bottom rounded-md shadow-fa-lg">
className="text-5xl font-bold" <h1
style={{ textShadow: "2px 2px 4px rgba(0,0,0,0.1)" }} className="text-5xl font-bold"
> style={{ textShadow: "2px 2px 4px rgba(0,0,0,0.1)" }}
Welcome to Thoughts >
</h1> Welcome to Thoughts
<p className="text-muted-foreground mt-2"> </h1>
Throwback to the golden age of microblogging. <p className="text-muted-foreground mt-2">
</p> Throwback to the golden age of microblogging.
<div className="mt-8 flex justify-center gap-4"> </p>
<Button asChild> <div className="mt-8 flex justify-center gap-4">
<Link href="/login">Login</Link> <Button asChild>
</Button> <Link href="/login">Login</Link>
<Button variant="secondary" asChild> </Button>
<Link href="/register">Register</Link> <Button variant="secondary" asChild>
</Button> <Link href="/register">Register</Link>
</Button>
</div>
</div> </div>
</div> </div>
</div> </>
); );
} }

View File

@@ -36,7 +36,7 @@ export function FollowButton({
setIsFollowing(!isFollowing); setIsFollowing(!isFollowing);
await action(username, token); await action(username, token);
router.refresh(); // Re-fetch server component data to get the latest follower count etc. router.refresh(); // Re-fetch server component data to get the latest follower count etc.
} catch (err) { } catch {
// Revert on error // Revert on error
setIsFollowing(isFollowing); setIsFollowing(isFollowing);
toast.error(`Failed to ${isFollowing ? "unfollow" : "follow"} user.`); toast.error(`Failed to ${isFollowing ? "unfollow" : "follow"} user.`);

View File

@@ -0,0 +1,107 @@
"use client";
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Download } from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardAction,
} from "@/components/ui/card";
interface CustomWindow extends Window {
MSStream?: unknown;
}
interface BeforeInstallPromptEvent extends Event {
prompt: () => void;
userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>;
}
export default function InstallPrompt() {
const [isIOS, setIsIOS] = useState(false);
const [isStandalone, setIsStandalone] = useState(false);
const [deferredPrompt, setDeferredPrompt] =
useState<BeforeInstallPromptEvent | null>(null);
useEffect(() => {
// Cast window to our custom type instead of 'any'
const customWindow = window as CustomWindow;
setIsIOS(
/iPad|iPhone|iPod/.test(navigator.userAgent) && !customWindow.MSStream
);
setIsStandalone(window.matchMedia("(display-mode: standalone)").matches);
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
};
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
return () => {
window.removeEventListener(
"beforeinstallprompt",
handleBeforeInstallPrompt
);
};
}, []);
const handleInstallClick = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === "accepted") {
console.log("User accepted the install prompt");
} else {
console.log("User dismissed the install prompt");
}
setDeferredPrompt(null);
};
if (isStandalone || (!isIOS && !deferredPrompt)) {
return null;
}
return (
<div className="fixed bottom-0 z-50">
<Card className="w-full max-w-sm glass-effect glossy-effect bottom shadow-fa-lg">
<CardHeader>
<CardTitle>Install Thoughts</CardTitle>
<CardDescription>
Get the full app experience on your device.
</CardDescription>
<CardAction>
<Button
size="sm"
variant="ghost"
className="absolute top-2 right-2"
onClick={() => setIsStandalone(true)}
>
&times;
</Button>
</CardAction>
</CardHeader>
<CardContent>
{!isIOS && deferredPrompt && (
<Button className="w-full" onClick={handleInstallClick}>
<Download className="mr-2 h-4 w-4" />
Add to Home Screen
</Button>
)}
{isIOS && (
<p className="text-sm text-muted-foreground">
To install, tap the Share icon
<span className="mx-1 text-lg"></span>
and then &quot;Add to Home Screen&quot;
<span className="mx-1 text-lg"></span>.
</p>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -17,7 +17,7 @@ const buttonVariants = cva(
"glass-effect fa-gradient-green text-secondary-foreground shadow-fa-md hover:bg-secondary/90 active:shadow-fa-inner transition-transform active:scale-[0.98] glossy-effect", "glass-effect fa-gradient-green text-secondary-foreground shadow-fa-md hover:bg-secondary/90 active:shadow-fa-inner transition-transform active:scale-[0.98] glossy-effect",
// Ghost and Link should be more subtle // Ghost and Link should be more subtle
ghost: ghost:
"glass-effect hover:bg-accent hover:text-accent-foreground rounded-lg", // Keep them simple, maybe a slight blur/gloss on hover "glass-effect hover:bg-accent hover:text-accent-foreground rounded-lg",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
// Outline button for a transparent-ish, glassy feel // Outline button for a transparent-ish, glassy feel
outline: outline:

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB