Skip to content

Commit 1731fc3

Browse files
committed
fixes and pwa support
1 parent 24797f7 commit 1731fc3

22 files changed

Lines changed: 4855 additions & 240 deletions

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ ENV NEXT_TELEMETRY_DISABLED=1
2727

2828
RUN yarn build
2929

30+
# Production image, copy all the files and run next
3031
FROM base AS runner
3132
WORKDIR /app
3233

@@ -36,6 +37,9 @@ ENV NEXT_TELEMETRY_DISABLED=1
3637
RUN addgroup --system --gid 1001 nodejs
3738
RUN adduser --system --uid 1001 nextjs
3839

40+
# Copy public directory
41+
COPY --from=builder /app/public ./public
42+
3943
# Set the correct permission for prerender cache
4044
RUN mkdir .next
4145
RUN chown nextjs:nodejs .next

app/(loggedOutRoutes)/auth/login/login-form.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
'use client'
1+
"use client";
22

3-
import { useState } from 'react'
4-
import { login } from '@/app/_server/actions/auth/login'
3+
import { useState } from "react";
4+
import { login } from "@/app/_server/actions/auth/login";
55

66
export default function LoginForm() {
7-
const [error, setError] = useState<string>('')
7+
const [error, setError] = useState<string>("");
8+
const [isLoading, setIsLoading] = useState(false);
89

910
async function handleSubmit(formData: FormData) {
10-
const result = await login(formData)
11-
if (result?.error) {
12-
setError(result.error)
11+
setIsLoading(true);
12+
setError("");
13+
14+
try {
15+
const result = await login(formData);
16+
if (result?.error) {
17+
setError(result.error);
18+
}
19+
} finally {
20+
setIsLoading(false);
1321
}
1422
}
1523

@@ -45,6 +53,7 @@ export default function LoginForm() {
4553
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
4654
placeholder="Enter your username"
4755
required
56+
disabled={isLoading}
4857
/>
4958
</div>
5059

@@ -62,16 +71,18 @@ export default function LoginForm() {
6271
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
6372
placeholder="Enter your password"
6473
required
74+
disabled={isLoading}
6575
/>
6676
</div>
6777

6878
<button
6979
type="submit"
80+
disabled={isLoading}
7081
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full"
7182
>
72-
Sign In
83+
{isLoading ? "Signing In..." : "Sign In"}
7384
</button>
7485
</form>
7586
</div>
76-
)
77-
}
87+
);
88+
}

app/(loggedOutRoutes)/auth/setup/setup-form.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,27 @@ import { register } from "@/app/_server/actions/auth/register";
55

66
export default function SetupForm() {
77
const [error, setError] = useState<string>("");
8+
const [isLoading, setIsLoading] = useState(false);
89

910
async function handleSubmit(formData: FormData) {
10-
const result = await register(formData);
11-
if (result?.error) {
12-
setError(result.error);
11+
setIsLoading(true);
12+
setError("");
13+
14+
try {
15+
const result = await register(formData);
16+
if (result?.error) {
17+
setError(result.error);
18+
}
19+
} finally {
20+
setIsLoading(false);
1321
}
1422
}
1523

1624
return (
1725
<div className="space-y-6">
1826
<div className="space-y-2 text-center">
1927
<h1 className="text-2xl font-bold tracking-tight text-foreground">
20-
Welcome to Checklist
28+
Welcome to rwMarkable
2129
</h1>
2230
<p className="text-sm text-muted-foreground">
2331
Create your admin account to get started
@@ -45,6 +53,7 @@ export default function SetupForm() {
4553
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
4654
placeholder="Choose a username"
4755
required
56+
disabled={isLoading}
4857
/>
4958
</div>
5059

@@ -62,6 +71,7 @@ export default function SetupForm() {
6271
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
6372
placeholder="Choose a strong password"
6473
required
74+
disabled={isLoading}
6575
/>
6676
</div>
6777

@@ -79,14 +89,16 @@ export default function SetupForm() {
7989
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
8090
placeholder="Confirm your password"
8191
required
92+
disabled={isLoading}
8293
/>
8394
</div>
8495

8596
<button
8697
type="submit"
98+
disabled={isLoading}
8799
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full"
88100
>
89-
Create Admin Account
101+
{isLoading ? "Creating Account..." : "Create Admin Account"}
90102
</button>
91103
</form>
92104
</div>

app/_components/ui/icons/test.svg

Lines changed: 23 additions & 5 deletions
Loading
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { Download, X } from "lucide-react";
5+
import { Button } from "@/app/_components/ui/elements/button";
6+
7+
export function InstallPrompt() {
8+
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
9+
const [showInstallPrompt, setShowInstallPrompt] = useState(false);
10+
const [isInstalled, setIsInstalled] = useState(false);
11+
12+
useEffect(() => {
13+
// Check if app is already installed
14+
if (window.matchMedia("(display-mode: standalone)").matches) {
15+
setIsInstalled(true);
16+
return;
17+
}
18+
19+
// Check if user has dismissed the prompt before
20+
const dismissed = localStorage.getItem("pwa-prompt-dismissed");
21+
if (dismissed) {
22+
return;
23+
}
24+
25+
// Listen for the beforeinstallprompt event
26+
const handleBeforeInstallPrompt = (e: Event) => {
27+
console.log("beforeinstallprompt event fired");
28+
e.preventDefault();
29+
setDeferredPrompt(e);
30+
setShowInstallPrompt(true);
31+
};
32+
33+
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
34+
35+
return () => {
36+
window.removeEventListener(
37+
"beforeinstallprompt",
38+
handleBeforeInstallPrompt
39+
);
40+
};
41+
}, []);
42+
43+
const handleInstallClick = async () => {
44+
if (deferredPrompt) {
45+
deferredPrompt.prompt();
46+
const { outcome } = await deferredPrompt.userChoice;
47+
48+
if (outcome === "accepted") {
49+
setIsInstalled(true);
50+
}
51+
52+
setDeferredPrompt(null);
53+
}
54+
55+
setShowInstallPrompt(false);
56+
};
57+
58+
const handleDismiss = () => {
59+
setShowInstallPrompt(false);
60+
localStorage.setItem("pwa-prompt-dismissed", "true");
61+
};
62+
63+
if (isInstalled || !showInstallPrompt) {
64+
return null;
65+
}
66+
67+
return (
68+
<div className="fixed bottom-4 left-4 right-4 z-50 bg-background border border-border rounded-lg shadow-lg p-4">
69+
<div className="flex items-center justify-between">
70+
<div className="flex items-center gap-3">
71+
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
72+
<Download className="h-5 w-5 text-primary-foreground" />
73+
</div>
74+
<div>
75+
<h3 className="font-medium text-foreground">Install rwMarkable</h3>
76+
<p className="text-sm text-muted-foreground">
77+
Add to your home screen for quick access
78+
</p>
79+
</div>
80+
</div>
81+
<div className="flex items-center gap-2">
82+
<Button
83+
onClick={handleInstallClick}
84+
size="sm"
85+
className="bg-primary text-primary-foreground hover:bg-primary/90"
86+
>
87+
Install
88+
</Button>
89+
<Button
90+
onClick={handleDismiss}
91+
variant="ghost"
92+
size="sm"
93+
className="h-8 w-8 p-0"
94+
>
95+
<X className="h-4 w-4" />
96+
</Button>
97+
</div>
98+
</div>
99+
</div>
100+
);
101+
}

app/layout.tsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,50 @@
1-
import type { Metadata } from "next";
1+
import type { Metadata, Viewport } from "next";
22
import { Inter } from "next/font/google";
33
import "@/app/_styles/globals.css";
44
import { ThemeProvider } from "@/app/_providers/ThemeProvider";
55
import { ChecklistProvider } from "@/app/_providers/ChecklistProvider";
66
import { AppModeProvider } from "@/app/_providers/AppModeProvider";
7+
import { InstallPrompt } from "@/app/_components/ui/pwa/InstallPrompt";
78

89
const inter = Inter({ subsets: ["latin"] });
910

1011
export const metadata: Metadata = {
1112
title: "rwMarkable",
12-
description: "Easily create and manage documents and checklists",
13+
description: "A simple, fast, and lightweight checklist application",
14+
manifest: "/app-icons/site.webmanifest",
15+
icons: {
16+
icon: [
17+
{
18+
url: "/app-icons/favicon-16x16.png",
19+
sizes: "16x16",
20+
type: "image/png",
21+
},
22+
{
23+
url: "/app-icons/favicon-32x32.png",
24+
sizes: "32x32",
25+
type: "image/png",
26+
},
27+
],
28+
apple: [
29+
{
30+
url: "/app-icons/apple-touch-icon.png",
31+
sizes: "180x180",
32+
type: "image/png",
33+
},
34+
],
35+
},
36+
appleWebApp: {
37+
capable: true,
38+
statusBarStyle: "default",
39+
title: "rwMarkable",
40+
},
41+
};
42+
43+
export const viewport: Viewport = {
44+
width: "device-width",
45+
initialScale: 1,
46+
maximumScale: 1,
47+
themeColor: "#000000",
1348
};
1449

1550
export default async function RootLayout({
@@ -19,12 +54,20 @@ export default async function RootLayout({
1954
}) {
2055
return (
2156
<html lang="en" suppressHydrationWarning>
57+
<head>
58+
<link rel="icon" href="/app-icons/favicon.ico" />
59+
<meta name="apple-mobile-web-app-capable" content="yes" />
60+
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
61+
<meta name="apple-mobile-web-app-title" content="rwMarkable" />
62+
<meta name="mobile-web-app-capable" content="yes" />
63+
</head>
2264
<body className={inter.className}>
2365
<ThemeProvider>
2466
<AppModeProvider>
2567
<ChecklistProvider>
2668
<div className="min-h-screen bg-background text-foreground transition-colors">
2769
{children}
70+
<InstallPrompt />
2871
</div>
2972
</ChecklistProvider>
3073
</AppModeProvider>

favicon.ico

15 KB
Binary file not shown.

next.config.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
const withPWA = require('next-pwa')({
2+
dest: 'public',
3+
register: true,
4+
skipWaiting: true,
5+
disable: process.env.NODE_ENV === 'development',
6+
buildExcludes: [/middleware-manifest\.json$/]
7+
})
8+
19
/** @type {import('next').NextConfig} */
210
const nextConfig = {
3-
output: 'standalone',
4-
images: {
5-
unoptimized: true,
6-
},
7-
compress: true,
8-
poweredByHeader: false,
11+
output: 'standalone',
912
}
1013

11-
module.exports = nextConfig
14+
module.exports = withPWA(nextConfig)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"lucide-react": "^0.294.0",
2323
"marked": "^16.1.2",
2424
"next": "14.0.4",
25+
"next-pwa": "^5.6.0",
2526
"next-themes": "^0.2.1",
2627
"react": "^18",
2728
"react-dom": "^18",
12.2 KB
Loading

0 commit comments

Comments
 (0)