@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.
- package/components/dashboard/installAppCard.tsx +187 -0
- package/dist/activity/queries.d.mts +1 -1
- package/dist/activity/queries.d.ts +1 -1
- package/dist/auth/helpers.d.mts +3 -3
- package/dist/auth/helpers.d.ts +3 -3
- package/dist/auth/helpers.js.map +1 -1
- package/dist/auth/helpers.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +1 -1
- package/dist/auth-client/index.d.mts +16 -16
- package/dist/auth-client/index.d.ts +16 -16
- package/dist/{chunk-BWUSJSHC.mjs → chunk-ONDXAY7W.mjs} +2 -2
- package/dist/chunk-ONDXAY7W.mjs.map +1 -0
- package/dist/db/index.d.mts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/schema.d.mts +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/{schema-BghJ8N4X.d.mts → schema-BrBmQFkn.d.mts} +1 -1
- package/dist/{schema-BghJ8N4X.d.ts → schema-BrBmQFkn.d.ts} +1 -1
- package/dist/settings/queries.d.mts +1 -1
- package/dist/settings/queries.d.ts +1 -1
- package/lib/device-fingerprint.ts +2 -3
- package/package.json +7 -31
- package/dist/chunk-BWUSJSHC.mjs.map +0 -1
|
@@ -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 “Open as Web App” 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'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'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
|
+
}
|
package/dist/auth/helpers.d.mts
CHANGED
|
@@ -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
|
}>;
|
package/dist/auth/helpers.d.ts
CHANGED
|
@@ -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
|
}>;
|