diff --git a/thoughts-frontend/app/(auth)/login/page.tsx b/thoughts-frontend/app/(auth)/login/page.tsx index c5edd91..963d8c6 100644 --- a/thoughts-frontend/app/(auth)/login/page.tsx +++ b/thoughts-frontend/app/(auth)/login/page.tsx @@ -42,7 +42,7 @@ export default function LoginPage() { const { token } = await loginUser(values); setToken(token); router.push("/"); // Redirect to homepage on successful login - } catch (err) { + } catch { setError("Invalid username or password."); } } diff --git a/thoughts-frontend/app/(auth)/register/page.tsx b/thoughts-frontend/app/(auth)/register/page.tsx index 0652aa3..96eaa2f 100644 --- a/thoughts-frontend/app/(auth)/register/page.tsx +++ b/thoughts-frontend/app/(auth)/register/page.tsx @@ -40,7 +40,7 @@ export default function RegisterPage() { await registerUser(values); // You can automatically log the user in here or just redirect them router.push("/login"); - } catch (err) { + } catch { setError("Username or email may already be taken."); } } diff --git a/thoughts-frontend/app/favicon.ico b/thoughts-frontend/app/favicon.ico index 718d6fe..ade0fe0 100644 Binary files a/thoughts-frontend/app/favicon.ico and b/thoughts-frontend/app/favicon.ico differ diff --git a/thoughts-frontend/app/layout.tsx b/thoughts-frontend/app/layout.tsx index 6ed15a2..9290d85 100644 --- a/thoughts-frontend/app/layout.tsx +++ b/thoughts-frontend/app/layout.tsx @@ -4,6 +4,7 @@ import { AuthProvider } from "@/hooks/use-auth"; import { Toaster } from "@/components/ui/sonner"; import { Header } from "@/components/header"; import localFont from "next/font/local"; +import InstallPrompt from "@/components/install-prompt"; export const metadata: Metadata = { title: "Thoughts", @@ -37,6 +38,7 @@ export default function RootLayout({
{children}
+ diff --git a/thoughts-frontend/app/manifest.ts b/thoughts-frontend/app/manifest.ts new file mode 100644 index 0000000..a2d28f7 --- /dev/null +++ b/thoughts-frontend/app/manifest.ts @@ -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', + }, + ], + } +} \ No newline at end of file diff --git a/thoughts-frontend/app/page.tsx b/thoughts-frontend/app/page.tsx index 48be557..c95dc9f 100644 --- a/thoughts-frontend/app/page.tsx +++ b/thoughts-frontend/app/page.tsx @@ -14,6 +14,7 @@ import { PopularTags } from "@/components/popular-tags"; import { ThoughtThread } from "@/components/thought-thread"; import { buildThoughtThreads } from "@/lib/utils"; import { TopFriends } from "@/components/top-friends"; +import InstallPrompt from "@/components/install-prompt"; export default async function Home() { const token = (await cookies()).get("auth_token")?.value ?? null; @@ -101,26 +102,28 @@ async function FeedPage({ token }: { token: string }) { function LandingPage() { return ( -
-
-

- Welcome to Thoughts -

-

- Throwback to the golden age of microblogging. -

-
- - + <> +
+
+

+ Welcome to Thoughts +

+

+ Throwback to the golden age of microblogging. +

+
+ + +
-
+ ); } diff --git a/thoughts-frontend/components/follow-button.tsx b/thoughts-frontend/components/follow-button.tsx index 7c552c9..0462601 100644 --- a/thoughts-frontend/components/follow-button.tsx +++ b/thoughts-frontend/components/follow-button.tsx @@ -36,7 +36,7 @@ export function FollowButton({ setIsFollowing(!isFollowing); await action(username, token); router.refresh(); // Re-fetch server component data to get the latest follower count etc. - } catch (err) { + } catch { // Revert on error setIsFollowing(isFollowing); toast.error(`Failed to ${isFollowing ? "unfollow" : "follow"} user.`); diff --git a/thoughts-frontend/components/install-prompt.tsx b/thoughts-frontend/components/install-prompt.tsx new file mode 100644 index 0000000..399b5da --- /dev/null +++ b/thoughts-frontend/components/install-prompt.tsx @@ -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(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 ( +
+ + + Install Thoughts + + Get the full app experience on your device. + + + + + + + {!isIOS && deferredPrompt && ( + + )} + {isIOS && ( +

+ To install, tap the Share icon + + and then "Add to Home Screen" + . +

+ )} +
+
+
+ ); +} diff --git a/thoughts-frontend/components/ui/button.tsx b/thoughts-frontend/components/ui/button.tsx index 6324de9..89df241 100644 --- a/thoughts-frontend/components/ui/button.tsx +++ b/thoughts-frontend/components/ui/button.tsx @@ -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", // Ghost and Link should be more subtle 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", // Outline button for a transparent-ish, glassy feel outline: diff --git a/thoughts-frontend/public/icon-128x128.webp b/thoughts-frontend/public/icon-128x128.webp new file mode 100644 index 0000000..3e84342 Binary files /dev/null and b/thoughts-frontend/public/icon-128x128.webp differ diff --git a/thoughts-frontend/public/icon-144x144.webp b/thoughts-frontend/public/icon-144x144.webp new file mode 100644 index 0000000..599cae1 Binary files /dev/null and b/thoughts-frontend/public/icon-144x144.webp differ diff --git a/thoughts-frontend/public/icon-152x152.webp b/thoughts-frontend/public/icon-152x152.webp new file mode 100644 index 0000000..fc5c27d Binary files /dev/null and b/thoughts-frontend/public/icon-152x152.webp differ diff --git a/thoughts-frontend/public/icon-192x192.webp b/thoughts-frontend/public/icon-192x192.webp new file mode 100644 index 0000000..3913435 Binary files /dev/null and b/thoughts-frontend/public/icon-192x192.webp differ diff --git a/thoughts-frontend/public/icon-256x256.webp b/thoughts-frontend/public/icon-256x256.webp new file mode 100644 index 0000000..e2cad87 Binary files /dev/null and b/thoughts-frontend/public/icon-256x256.webp differ diff --git a/thoughts-frontend/public/icon-384x384.webp b/thoughts-frontend/public/icon-384x384.webp new file mode 100644 index 0000000..b529d52 Binary files /dev/null and b/thoughts-frontend/public/icon-384x384.webp differ diff --git a/thoughts-frontend/public/icon-48x48.webp b/thoughts-frontend/public/icon-48x48.webp new file mode 100644 index 0000000..e0ddcfa Binary files /dev/null and b/thoughts-frontend/public/icon-48x48.webp differ diff --git a/thoughts-frontend/public/icon-512x512.webp b/thoughts-frontend/public/icon-512x512.webp new file mode 100644 index 0000000..0271431 Binary files /dev/null and b/thoughts-frontend/public/icon-512x512.webp differ diff --git a/thoughts-frontend/public/icon-72x72.webp b/thoughts-frontend/public/icon-72x72.webp new file mode 100644 index 0000000..ef0f2f2 Binary files /dev/null and b/thoughts-frontend/public/icon-72x72.webp differ diff --git a/thoughts-frontend/public/icon-96x96.webp b/thoughts-frontend/public/icon-96x96.webp new file mode 100644 index 0000000..bd18eaf Binary files /dev/null and b/thoughts-frontend/public/icon-96x96.webp differ diff --git a/thoughts-frontend/public/icon.avif b/thoughts-frontend/public/icon.avif new file mode 100644 index 0000000..84191ae Binary files /dev/null and b/thoughts-frontend/public/icon.avif differ