feat: build production-ready wtr weather PWA
This commit is contained in:
25
components/ui/button.tsx
Normal file
25
components/ui/button.tsx
Normal 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
6
components/ui/card.tsx
Normal 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} />;
|
||||
}
|
||||
37
components/ui/install-pwa-button.tsx
Normal file
37
components/ui/install-pwa-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
48
components/ui/theme-toggle.tsx
Normal file
48
components/ui/theme-toggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user