@xk2800/nextjs-template 0.8.0-beta.0 → 0.8.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, type ReactNode } from 'react'
4
+ import { CircleCheck, Download } from 'lucide-react'
5
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../ui/card'
6
+ import { Button } from '../ui/button'
7
+
8
+ // Chrome/Edge's install event; not in the DOM typings yet.
9
+ type InstallPromptEvent = Event & { prompt: () => Promise<{ outcome: 'accepted' | 'dismissed' }> }
10
+
11
+ declare global {
12
+ interface Window {
13
+ // Stashed by app/layout.tsx in case the event fires before this card mounts.
14
+ __pwaInstallPrompt?: InstallPromptEvent
15
+ }
16
+ }
17
+
18
+ export type InstallTarget = {
19
+ os: 'ios' | 'android' | 'desktop'
20
+ browser: 'safari' | 'chrome' | 'edge' | 'firefox' | 'samsung' | 'other'
21
+ }
22
+
23
+ // iPadOS reports a Mac user agent, so touch support tells them apart.
24
+ export function detectInstallTarget(ua: string, maxTouchPoints = 0): InstallTarget {
25
+ const ios = /iPhone|iPad|iPod/.test(ua) || (/Macintosh/.test(ua) && maxTouchPoints > 1)
26
+ const os = ios ? 'ios' : /Android/.test(ua) ? 'android' : 'desktop'
27
+ const browser = /SamsungBrowser/.test(ua)
28
+ ? 'samsung'
29
+ : /Edg(e|A|iOS)?\//.test(ua)
30
+ ? 'edge'
31
+ : /Firefox|FxiOS/.test(ua)
32
+ ? 'firefox'
33
+ : /OPR\/|Opera/.test(ua)
34
+ ? 'other'
35
+ : /Chrome|CriOS/.test(ua)
36
+ ? 'chrome'
37
+ : /Safari/.test(ua)
38
+ ? 'safari'
39
+ : 'other'
40
+ return { os, browser }
41
+ }
42
+
43
+ const BROWSER_NAMES: Record<InstallTarget['browser'], string> = {
44
+ safari: 'Safari',
45
+ chrome: 'Chrome',
46
+ edge: 'Edge',
47
+ firefox: 'Firefox',
48
+ samsung: 'Samsung Internet',
49
+ other: 'your browser',
50
+ }
51
+ const OS_NAMES: Record<InstallTarget['os'], string> = {
52
+ ios: 'iPhone / iPad',
53
+ android: 'Android',
54
+ desktop: 'this computer',
55
+ }
56
+
57
+ // Manual steps for each OS/browser, or null when the browser can't install apps.
58
+ function steps({ os, browser }: InstallTarget): ReactNode[] | null {
59
+ if (os === 'ios') {
60
+ return [
61
+ browser === 'safari' ? (
62
+ <>Tap <strong>Share</strong> in the toolbar. It may be inside the <strong>⋯</strong> menu.</>
63
+ ) : (
64
+ <>Tap <strong>Share</strong> in the address bar or menu.</>
65
+ ),
66
+ <>Choose <strong>Add to Home Screen</strong>. Keep &ldquo;Open as Web App&rdquo; on if shown.</>,
67
+ <>Tap <strong>Add</strong>, then open the new icon on your home screen.</>,
68
+ ]
69
+ }
70
+ if (os === 'android') {
71
+ if (browser === 'samsung') {
72
+ return [
73
+ <>Tap the <strong>☰</strong> menu at the bottom right.</>,
74
+ <>Choose <strong>Add page to</strong> → <strong>Home screen</strong>.</>,
75
+ ]
76
+ }
77
+ return [
78
+ <>Tap the <strong>⋮</strong> menu at the top right.</>,
79
+ <>Choose <strong>Install app</strong> or <strong>Add to Home screen</strong>.</>,
80
+ ]
81
+ }
82
+ if (browser === 'chrome') {
83
+ return [
84
+ <>
85
+ Click the install icon at the right of the address bar, or open the <strong>⋮</strong> menu
86
+ and choose <strong>Cast, save and share</strong> → <strong>Install page as app</strong>.
87
+ </>,
88
+ ]
89
+ }
90
+ if (browser === 'edge') {
91
+ return [
92
+ <>
93
+ Click the install icon at the right of the address bar, or open the <strong>⋯</strong> menu
94
+ and choose <strong>Apps</strong> → <strong>Install this site as an app</strong>.
95
+ </>,
96
+ ]
97
+ }
98
+ if (browser === 'safari') {
99
+ return [<>Choose <strong>File</strong> → <strong>Add to Dock</strong> (macOS Sonoma or later).</>]
100
+ }
101
+ return null
102
+ }
103
+
104
+ // Install instructions for the visitor's own OS and browser, plus a one-click
105
+ // Install button where the browser offers one (Chrome, Edge). Shows a
106
+ // confirmation instead once the app runs installed.
107
+ export default function InstallAppCard() {
108
+ const [target, setTarget] = useState<InstallTarget | undefined>()
109
+ const [installed, setInstalled] = useState(false)
110
+ const [prompt, setPrompt] = useState<InstallPromptEvent | undefined>()
111
+
112
+ useEffect(() => {
113
+ setTarget(detectInstallTarget(navigator.userAgent, navigator.maxTouchPoints))
114
+ setInstalled(
115
+ matchMedia('(display-mode: standalone)').matches ||
116
+ (navigator as Navigator & { standalone?: boolean }).standalone === true
117
+ )
118
+ setPrompt(window.__pwaInstallPrompt)
119
+
120
+ const onPrompt = (e: Event) => setPrompt(e as InstallPromptEvent)
121
+ const onInstalled = () => setInstalled(true)
122
+ addEventListener('beforeinstallprompt', onPrompt)
123
+ addEventListener('appinstalled', onInstalled)
124
+ return () => {
125
+ removeEventListener('beforeinstallprompt', onPrompt)
126
+ removeEventListener('appinstalled', onInstalled)
127
+ }
128
+ }, [])
129
+
130
+ async function install() {
131
+ if (!prompt) return
132
+ const { outcome } = await prompt.prompt()
133
+ // The event can only be used once.
134
+ window.__pwaInstallPrompt = undefined
135
+ setPrompt(undefined)
136
+ if (outcome === 'accepted') setInstalled(true)
137
+ }
138
+
139
+ // Rendered after mount only: the server can't see the visitor's device.
140
+ if (!target) return null
141
+ const list = steps(target)
142
+
143
+ return (
144
+ <Card>
145
+ <CardHeader>
146
+ <CardTitle>Install the app</CardTitle>
147
+ <CardDescription>
148
+ Open it from your home screen or dock, in its own window.
149
+ </CardDescription>
150
+ </CardHeader>
151
+ <CardContent className="space-y-4">
152
+ {installed ? (
153
+ <p className="flex items-center gap-2 text-sm">
154
+ <CircleCheck className="size-4 text-green-600" aria-hidden />
155
+ You&apos;re using the installed app on this device.
156
+ </p>
157
+ ) : (
158
+ <>
159
+ <p className="text-sm text-muted-foreground">
160
+ {BROWSER_NAMES[target.browser]} on {OS_NAMES[target.os]}
161
+ </p>
162
+ {prompt && (
163
+ <Button onClick={install}>
164
+ <Download aria-hidden /> Install app
165
+ </Button>
166
+ )}
167
+ {list ? (
168
+ <ol className="list-decimal space-y-2 pl-5 text-sm marker:text-muted-foreground">
169
+ {list.map((step, i) => (
170
+ <li key={i}>{step}</li>
171
+ ))}
172
+ </ol>
173
+ ) : (
174
+ !prompt && (
175
+ <p className="text-sm">
176
+ This browser can&apos;t install apps. Open this page in{' '}
177
+ {target.os === 'desktop' ? 'Chrome, Edge, or Safari on a Mac' : 'Chrome'} to install
178
+ it.
179
+ </p>
180
+ )
181
+ )}
182
+ </>
183
+ )}
184
+ </CardContent>
185
+ </Card>
186
+ )
187
+ }
@@ -1,4 +1,4 @@
1
- import { A as ActivityActionEnum } from '../schema-BghJ8N4X.mjs';
1
+ import { A as ActivityActionEnum } from '../schema-BrBmQFkn.mjs';
2
2
  import 'drizzle-orm';
3
3
  import 'drizzle-orm/pg-core';
4
4
 
@@ -1,4 +1,4 @@
1
- import { A as ActivityActionEnum } from '../schema-BghJ8N4X.js';
1
+ import { A as ActivityActionEnum } from '../schema-BrBmQFkn.js';
2
2
  import 'drizzle-orm';
3
3
  import 'drizzle-orm/pg-core';
4
4
 
@@ -27,8 +27,8 @@ declare const getCurrentSession: () => Promise<{
27
27
  image?: string | null | undefined | undefined;
28
28
  role: string | null | undefined;
29
29
  banned: boolean | null | undefined;
30
- banReason?: string | null | undefined;
31
30
  banExpires?: Date | null | undefined;
31
+ banReason?: string | null | undefined;
32
32
  twoFactorEnabled: boolean | null | undefined;
33
33
  };
34
34
  } | null>;
@@ -61,8 +61,8 @@ declare const requireAuth: () => Promise<{
61
61
  image?: string | null | undefined | undefined;
62
62
  role: string | null | undefined;
63
63
  banned: boolean | null | undefined;
64
- banReason?: string | null | undefined;
65
64
  banExpires?: Date | null | undefined;
65
+ banReason?: string | null | undefined;
66
66
  twoFactorEnabled: boolean | null | undefined;
67
67
  };
68
68
  }>;
@@ -95,8 +95,8 @@ declare function requireRole(requiredRole: 'user' | 'admin'): Promise<{
95
95
  image?: string | null | undefined | undefined;
96
96
  role: string | null | undefined;
97
97
  banned: boolean | null | undefined;
98
- banReason?: string | null | undefined;
99
98
  banExpires?: Date | null | undefined;
99
+ banReason?: string | null | undefined;
100
100
  twoFactorEnabled: boolean | null | undefined;
101
101
  };
102
102
  }>;
@@ -27,8 +27,8 @@ declare const getCurrentSession: () => Promise<{
27
27
  image?: string | null | undefined | undefined;
28
28
  role: string | null | undefined;
29
29
  banned: boolean | null | undefined;
30
- banReason?: string | null | undefined;
31
30
  banExpires?: Date | null | undefined;
31
+ banReason?: string | null | undefined;
32
32
  twoFactorEnabled: boolean | null | undefined;
33
33
  };
34
34
  } | null>;
@@ -61,8 +61,8 @@ declare const requireAuth: () => Promise<{
61
61
  image?: string | null | undefined | undefined;
62
62
  role: string | null | undefined;
63
63
  banned: boolean | null | undefined;
64
- banReason?: string | null | undefined;
65
64
  banExpires?: Date | null | undefined;
65
+ banReason?: string | null | undefined;
66
66
  twoFactorEnabled: boolean | null | undefined;
67
67
  };
68
68
  }>;
@@ -95,8 +95,8 @@ declare function requireRole(requiredRole: 'user' | 'admin'): Promise<{
95
95
  image?: string | null | undefined | undefined;
96
96
  role: string | null | undefined;
97
97
  banned: boolean | null | undefined;
98
- banReason?: string | null | undefined;
99
98
  banExpires?: Date | null | undefined;
99
+ banReason?: string | null | undefined;
100
100
  twoFactorEnabled: boolean | null | undefined;
101
101
  };
102
102
  }>;