feat: build production-ready wtr weather PWA

This commit is contained in:
zv
2026-06-01 18:43:56 +02:00
commit 840555f4f5
60 changed files with 9052 additions and 0 deletions

25
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,25 @@
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-full text-sm font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-slate-950 px-4 py-2.5 text-white hover:bg-slate-800 dark:bg-white dark:text-slate-950 dark:hover:bg-slate-200",
glass: "border border-white/30 bg-white/30 px-4 py-2.5 text-slate-800 backdrop-blur-xl hover:bg-white/50 dark:border-white/10 dark:bg-white/10 dark:text-white dark:hover:bg-white/20",
ghost: "px-3 py-2 text-slate-700 hover:bg-white/50 dark:text-slate-200 dark:hover:bg-white/10",
icon: "size-10 border border-white/30 bg-white/30 text-slate-800 backdrop-blur-xl hover:bg-white/50 dark:border-white/10 dark:bg-white/10 dark:text-white dark:hover:bg-white/20",
},
},
defaultVariants: { variant: "default" },
},
);
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, ...props }, ref) => (
<button ref={ref} className={cn(buttonVariants({ variant }), className)} {...props} />
));
Button.displayName = "Button";

6
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,6 @@
import type { HTMLAttributes } from "react";
import { cn } from "@/lib/utils";
export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
return <div className={cn("glass rounded-[1.75rem]", className)} {...props} />;
}

View File

@@ -0,0 +1,37 @@
"use client";
import { useEffect, useState } from "react";
import { Download } from "lucide-react";
import { Button } from "@/components/ui/button";
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
}
export function InstallPWAButton() {
const [event, setEvent] = useState<BeforeInstallPromptEvent | null>(null);
useEffect(() => {
const handlePrompt = (promptEvent: Event) => {
promptEvent.preventDefault();
setEvent(promptEvent as BeforeInstallPromptEvent);
};
window.addEventListener("beforeinstallprompt", handlePrompt);
return () => window.removeEventListener("beforeinstallprompt", handlePrompt);
}, []);
if (!event) return null;
return (
<Button
variant="glass"
onClick={async () => {
await event.prompt();
await event.userChoice;
setEvent(null);
}}
>
<Download className="size-4" />
Zainstaluj
</Button>
);
}

View File

@@ -0,0 +1,48 @@
"use client";
import { useEffect, useState } from "react";
import { Moon, Sun, SunMoon } from "lucide-react";
import { Button } from "@/components/ui/button";
export function ThemeToggle() {
const [mounted, setMounted] = useState(false);
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const root = document.documentElement;
const media = window.matchMedia("(prefers-color-scheme: dark)");
const syncSystemTheme = () => {
if (!window.localStorage.getItem("wtr:theme")) {
root.classList.toggle("dark", media.matches);
setIsDark(media.matches);
}
};
const animationFrame = window.requestAnimationFrame(() => {
setIsDark(root.classList.contains("dark"));
setMounted(true);
});
media.addEventListener("change", syncSystemTheme);
return () => {
window.cancelAnimationFrame(animationFrame);
media.removeEventListener("change", syncSystemTheme);
};
}, []);
const toggleTheme = () => {
const nextIsDark = !isDark;
document.documentElement.classList.toggle("dark", nextIsDark);
window.localStorage.setItem("wtr:theme", nextIsDark ? "dark" : "light");
setIsDark(nextIsDark);
};
return (
<Button
variant="icon"
type="button"
aria-label={!mounted ? "Zmień motyw" : isDark ? "Włącz jasny motyw" : "Włącz ciemny motyw"}
onClick={toggleTheme}
>
{!mounted ? <SunMoon className="size-4" /> : isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
</Button>
);
}