@meith/web 0.30.0 → 0.30.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/package.json +48 -48
- package/src/components/shell/install-action.tsx +86 -0
- package/src/components/shell/install-banner.tsx +29 -0
- package/src/components/shell/page-shell.tsx +18 -7
- package/src/components/shell/theme-switcher.tsx +52 -59
- package/src/server/install-banner-actions.ts +18 -0
- package/src/server/install-banner.ts +29 -0
- package/src/server/upgrade-notice.ts +1 -1
- package/src/theme/contract.fixture.ts +2 -1
- package/src/view/install-banner.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "The board itself: the Next.js app, and the forum-web bin that materializes it into an external board workspace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,53 +43,53 @@
|
|
|
43
43
|
"react-dom": "19.2.8",
|
|
44
44
|
"tailwindcss": "^4.3.3",
|
|
45
45
|
"typescript": "7.0.2",
|
|
46
|
-
"@meith/accounts": "0.30.
|
|
47
|
-
"@meith/admin": "0.30.
|
|
48
|
-
"@meith/antispam": "0.30.
|
|
49
|
-
"@meith/api": "0.30.
|
|
50
|
-
"@meith/attachments": "0.30.
|
|
51
|
-
"@meith/authorization": "0.30.
|
|
52
|
-
"@meith/avatars": "0.30.
|
|
53
|
-
"@meith/core": "0.30.
|
|
54
|
-
"@meith/db": "0.30.
|
|
55
|
-
"@meith/
|
|
56
|
-
"@meith/
|
|
57
|
-
"@meith/drivers": "0.30.
|
|
58
|
-
"@meith/events": "0.30.
|
|
59
|
-
"@meith/
|
|
60
|
-
"@meith/
|
|
61
|
-
"@meith/i18n": "0.30.
|
|
62
|
-
"@meith/import": "0.30.
|
|
63
|
-
"@meith/install": "0.30.
|
|
64
|
-
"@meith/mail": "0.30.
|
|
65
|
-
"@meith/markdown": "0.30.
|
|
66
|
-
"@meith/marketplace": "0.30.
|
|
67
|
-
"@meith/messages": "0.30.
|
|
68
|
-
"@meith/moderation": "0.30.
|
|
69
|
-
"@meith/notifications": "0.30.
|
|
70
|
-
"@meith/plugin-calendar": "0.30.
|
|
71
|
-
"@meith/plugin-dues": "0.30.
|
|
72
|
-
"@meith/plugin-kit": "0.30.
|
|
73
|
-
"@meith/polls": "0.30.
|
|
74
|
-
"@meith/posts": "0.30.
|
|
75
|
-
"@meith/profile-fields": "0.30.
|
|
76
|
-
"@meith/relations": "0.30.
|
|
77
|
-
"@meith/reputation": "0.30.
|
|
78
|
-
"@meith/runtime": "0.30.
|
|
79
|
-
"@meith/search": "0.30.
|
|
80
|
-
"@meith/settings": "0.30.
|
|
81
|
-
"@meith/signatures": "0.30.
|
|
82
|
-
"@meith/subscriptions": "0.30.
|
|
83
|
-
"@meith/tasks": "0.30.
|
|
84
|
-
"@meith/theme-clubhouse": "0.30.
|
|
85
|
-
"@meith/theme-default": "0.30.
|
|
86
|
-
"@meith/theme-kit": "0.30.
|
|
87
|
-
"@meith/theme-midnight": "0.30.
|
|
88
|
-
"@meith/theme-phasebook": "0.30.
|
|
89
|
-
"@meith/theme-raidframe": "0.30.
|
|
90
|
-
"@meith/threads": "0.30.
|
|
91
|
-
"@meith/ui": "0.30.
|
|
92
|
-
"@meith/upgrade": "0.30.
|
|
46
|
+
"@meith/accounts": "0.30.1",
|
|
47
|
+
"@meith/admin": "0.30.1",
|
|
48
|
+
"@meith/antispam": "0.30.1",
|
|
49
|
+
"@meith/api": "0.30.1",
|
|
50
|
+
"@meith/attachments": "0.30.1",
|
|
51
|
+
"@meith/authorization": "0.30.1",
|
|
52
|
+
"@meith/avatars": "0.30.1",
|
|
53
|
+
"@meith/core": "0.30.1",
|
|
54
|
+
"@meith/db": "0.30.1",
|
|
55
|
+
"@meith/drafts": "0.30.1",
|
|
56
|
+
"@meith/demo": "0.30.1",
|
|
57
|
+
"@meith/drivers": "0.30.1",
|
|
58
|
+
"@meith/events": "0.30.1",
|
|
59
|
+
"@meith/groups": "0.30.1",
|
|
60
|
+
"@meith/forums": "0.30.1",
|
|
61
|
+
"@meith/i18n": "0.30.1",
|
|
62
|
+
"@meith/import": "0.30.1",
|
|
63
|
+
"@meith/install": "0.30.1",
|
|
64
|
+
"@meith/mail": "0.30.1",
|
|
65
|
+
"@meith/markdown": "0.30.1",
|
|
66
|
+
"@meith/marketplace": "0.30.1",
|
|
67
|
+
"@meith/messages": "0.30.1",
|
|
68
|
+
"@meith/moderation": "0.30.1",
|
|
69
|
+
"@meith/notifications": "0.30.1",
|
|
70
|
+
"@meith/plugin-calendar": "0.30.1",
|
|
71
|
+
"@meith/plugin-dues": "0.30.1",
|
|
72
|
+
"@meith/plugin-kit": "0.30.1",
|
|
73
|
+
"@meith/polls": "0.30.1",
|
|
74
|
+
"@meith/posts": "0.30.1",
|
|
75
|
+
"@meith/profile-fields": "0.30.1",
|
|
76
|
+
"@meith/relations": "0.30.1",
|
|
77
|
+
"@meith/reputation": "0.30.1",
|
|
78
|
+
"@meith/runtime": "0.30.1",
|
|
79
|
+
"@meith/search": "0.30.1",
|
|
80
|
+
"@meith/settings": "0.30.1",
|
|
81
|
+
"@meith/signatures": "0.30.1",
|
|
82
|
+
"@meith/subscriptions": "0.30.1",
|
|
83
|
+
"@meith/tasks": "0.30.1",
|
|
84
|
+
"@meith/theme-clubhouse": "0.30.1",
|
|
85
|
+
"@meith/theme-default": "0.30.1",
|
|
86
|
+
"@meith/theme-kit": "0.30.1",
|
|
87
|
+
"@meith/theme-midnight": "0.30.1",
|
|
88
|
+
"@meith/theme-phasebook": "0.30.1",
|
|
89
|
+
"@meith/theme-raidframe": "0.30.1",
|
|
90
|
+
"@meith/threads": "0.30.1",
|
|
91
|
+
"@meith/ui": "0.30.1",
|
|
92
|
+
"@meith/upgrade": "0.30.1"
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
|
95
95
|
"dev": "next dev",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { INSTALL_BANNER_COOKIE, INSTALL_BANNER_COOKIE_MAX_AGE } from '@/view/install-banner'
|
|
6
|
+
|
|
7
|
+
interface InstallPrompt extends Event {
|
|
8
|
+
prompt(): Promise<void>
|
|
9
|
+
readonly userChoice: Promise<{ readonly outcome: 'accepted' | 'dismissed' }>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CookieStoreLike {
|
|
13
|
+
set(init: {
|
|
14
|
+
readonly name: string
|
|
15
|
+
readonly value: string
|
|
16
|
+
readonly path: string
|
|
17
|
+
readonly expires: number
|
|
18
|
+
readonly sameSite: 'lax'
|
|
19
|
+
}): Promise<void>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isInstallPrompt(event: Event): event is InstallPrompt {
|
|
23
|
+
return 'prompt' in event && 'userChoice' in event
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const BUTTON =
|
|
27
|
+
'inline-flex h-8 items-center rounded-md border border-border bg-background px-3 text-xs font-medium text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
28
|
+
|
|
29
|
+
export function InstallAction({ label, how }: { label: string; how: string }) {
|
|
30
|
+
const [pending, setPending] = useState<InstallPrompt | null>(null)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const capture = (event: Event) => {
|
|
34
|
+
if (!isInstallPrompt(event)) return
|
|
35
|
+
event.preventDefault()
|
|
36
|
+
setPending(event)
|
|
37
|
+
}
|
|
38
|
+
const installed = () => setPending(null)
|
|
39
|
+
|
|
40
|
+
window.addEventListener('beforeinstallprompt', capture)
|
|
41
|
+
window.addEventListener('appinstalled', installed)
|
|
42
|
+
return () => {
|
|
43
|
+
window.removeEventListener('beforeinstallprompt', capture)
|
|
44
|
+
window.removeEventListener('appinstalled', installed)
|
|
45
|
+
}
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
if (pending !== null) {
|
|
49
|
+
const install = () => {
|
|
50
|
+
const prompt = pending
|
|
51
|
+
setPending(null)
|
|
52
|
+
prompt
|
|
53
|
+
.prompt()
|
|
54
|
+
.then(() => prompt.userChoice)
|
|
55
|
+
.then((choice) => {
|
|
56
|
+
if (choice.outcome !== 'accepted') return
|
|
57
|
+
const store = (window as { cookieStore?: CookieStoreLike }).cookieStore
|
|
58
|
+
return store?.set({
|
|
59
|
+
name: INSTALL_BANNER_COOKIE,
|
|
60
|
+
value: '1',
|
|
61
|
+
path: '/',
|
|
62
|
+
expires: Date.now() + INSTALL_BANNER_COOKIE_MAX_AGE * 1000,
|
|
63
|
+
sameSite: 'lax',
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
.catch(() => {})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<button type="button" onClick={install} className={BUTTON}>
|
|
71
|
+
{label}
|
|
72
|
+
</button>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<details>
|
|
78
|
+
<summary className={`${BUTTON} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}>
|
|
79
|
+
{label}
|
|
80
|
+
</summary>
|
|
81
|
+
<p className="absolute inset-x-0 bottom-full z-10 mb-2 rounded-md border border-border bg-card p-3 text-xs text-muted-foreground shadow-lg">
|
|
82
|
+
{how}
|
|
83
|
+
</p>
|
|
84
|
+
</details>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PendingButton } from '@/components/auth/form-controls'
|
|
2
|
+
import { InstallAction } from '@/components/shell/install-action'
|
|
3
|
+
import { BOARD_MEASURE } from '@/components/shell/measure'
|
|
4
|
+
import type { InstallBannerModel } from '@/server/install-banner'
|
|
5
|
+
import { dismissInstallBannerAction } from '@/server/install-banner-actions'
|
|
6
|
+
|
|
7
|
+
export function InstallBanner({ message, how, installLabel, dismissLabel }: InstallBannerModel) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="fixed inset-x-0 bottom-0 z-40 border-t border-border bg-card text-sm text-foreground shadow-lg md:hidden [@media(display-mode:standalone)]:hidden">
|
|
10
|
+
<div
|
|
11
|
+
className={`${BOARD_MEASURE} relative flex flex-wrap items-center justify-between gap-x-4 gap-y-2 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]`}
|
|
12
|
+
>
|
|
13
|
+
<p>{message}</p>
|
|
14
|
+
|
|
15
|
+
<div className="flex items-center gap-3">
|
|
16
|
+
<InstallAction label={installLabel} how={how} />
|
|
17
|
+
<form action={dismissInstallBannerAction}>
|
|
18
|
+
<PendingButton
|
|
19
|
+
showWorking
|
|
20
|
+
className="text-xs text-muted-foreground hover:text-foreground"
|
|
21
|
+
>
|
|
22
|
+
{dismissLabel}
|
|
23
|
+
</PendingButton>
|
|
24
|
+
</form>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -3,6 +3,7 @@ import { currentRequestId } from '@meith/core/logger'
|
|
|
3
3
|
import { requireSlot, slotCopy } from '@meith/theme-kit'
|
|
4
4
|
|
|
5
5
|
import { LogoutForm } from '@/components/account/logout-form'
|
|
6
|
+
import { InstallBanner } from '@/components/shell/install-banner'
|
|
6
7
|
import { NotificationMenu } from '@/components/shell/notification-menu'
|
|
7
8
|
import { OnboardingBanner } from '@/components/shell/onboarding-banner'
|
|
8
9
|
import { ThemeSwitcher } from '@/components/shell/theme-switcher'
|
|
@@ -10,6 +11,7 @@ import { avatarsFor } from '@/server/avatars'
|
|
|
10
11
|
import { currentLogo } from '@/server/branding'
|
|
11
12
|
import { getContainer } from '@/server/container'
|
|
12
13
|
import { getTranslator } from '@/server/i18n'
|
|
14
|
+
import { installBanner } from '@/server/install-banner'
|
|
13
15
|
import { legalFooterLinks } from '@/server/legal'
|
|
14
16
|
import { unreadMessageCount } from '@/server/messages'
|
|
15
17
|
import { boardNavigation } from '@/server/navigation'
|
|
@@ -119,6 +121,7 @@ export async function PageShell({ actor, children }: { actor: Actor; children: R
|
|
|
119
121
|
const jump = built === null ? null : await filterView('view.forum-jump', built, pluginContext)
|
|
120
122
|
|
|
121
123
|
const onboarding = await onboardingBanner(actor).catch(() => null)
|
|
124
|
+
const install = await installBanner().catch(() => null)
|
|
122
125
|
|
|
123
126
|
const translator = await getTranslator()
|
|
124
127
|
|
|
@@ -151,17 +154,25 @@ export async function PageShell({ actor, children }: { actor: Actor; children: R
|
|
|
151
154
|
|
|
152
155
|
{await boardRegion('header.notice', actor)}
|
|
153
156
|
|
|
157
|
+
{install !== null && <InstallBanner {...install} />}
|
|
154
158
|
{onboarding !== null && <OnboardingBanner {...onboarding} />}
|
|
155
159
|
|
|
156
160
|
{children}
|
|
157
161
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
<Footer
|
|
163
|
+
{...footerModel}
|
|
164
|
+
regions={{
|
|
165
|
+
controls: (
|
|
166
|
+
<>
|
|
167
|
+
{jump !== null && jump.forums.length > 0 && (
|
|
168
|
+
<ForumJump {...jump} copy={slotCopy(theme, 'ForumJump', translator)} />
|
|
169
|
+
)}
|
|
170
|
+
<ThemeSwitcher />
|
|
171
|
+
</>
|
|
172
|
+
),
|
|
173
|
+
}}
|
|
174
|
+
copy={slotCopy(theme, 'Footer', translator)}
|
|
175
|
+
/>
|
|
165
176
|
</Shell>
|
|
166
177
|
)
|
|
167
178
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { PendingButton } from '@/components/auth/form-controls'
|
|
2
2
|
import { MonitorIcon, MoonIcon, SunIcon } from '@/components/shell/appearance-icons'
|
|
3
|
-
import { BOARD_MEASURE } from '@/components/shell/measure'
|
|
4
3
|
import { setAppearanceAction } from '@/server/appearance-actions'
|
|
5
4
|
import { getTranslator } from '@/server/i18n'
|
|
6
5
|
import { currentColourScheme, currentThemeKey } from '@/server/theme'
|
|
@@ -31,67 +30,61 @@ export async function ThemeSwitcher() {
|
|
|
31
30
|
])
|
|
32
31
|
|
|
33
32
|
return (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
<section
|
|
34
|
+
aria-label={t.t('appearance.section')}
|
|
35
|
+
className="flex flex-wrap items-center justify-end gap-x-4 gap-y-2"
|
|
36
|
+
>
|
|
37
|
+
{choices.length > 1 && (
|
|
38
|
+
<form action={setAppearanceAction} className="flex items-center gap-2">
|
|
39
|
+
<label htmlFor="appearance-theme" className="text-xs text-muted-foreground">
|
|
40
|
+
{t.t('appearance.theme')}
|
|
41
|
+
</label>
|
|
42
|
+
<select id="appearance-theme" name="theme" defaultValue={theme} className={CONTROL}>
|
|
43
|
+
{choices.map((choice) => (
|
|
44
|
+
<option key={choice.key} value={choice.key}>
|
|
45
|
+
{choice.title}
|
|
46
|
+
</option>
|
|
47
|
+
))}
|
|
48
|
+
</select>
|
|
49
|
+
<PendingButton
|
|
50
|
+
showWorking
|
|
51
|
+
className="h-8 rounded-md border border-border px-2.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
52
|
+
>
|
|
53
|
+
{t.t('appearance.apply')}
|
|
54
|
+
</PendingButton>
|
|
55
|
+
</form>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
<form action={setAppearanceAction}>
|
|
59
|
+
<span
|
|
60
|
+
role="group"
|
|
61
|
+
aria-label={t.t('appearance.schemeGroup')}
|
|
62
|
+
className="inline-flex items-center overflow-hidden rounded-md border border-border text-muted-foreground"
|
|
41
63
|
>
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<select id="appearance-theme" name="theme" defaultValue={theme} className={CONTROL}>
|
|
48
|
-
{choices.map((choice) => (
|
|
49
|
-
<option key={choice.key} value={choice.key}>
|
|
50
|
-
{choice.title}
|
|
51
|
-
</option>
|
|
52
|
-
))}
|
|
53
|
-
</select>
|
|
64
|
+
{COLOUR_SCHEMES.map((option) => {
|
|
65
|
+
const Icon = SCHEME_ICON[option]
|
|
66
|
+
const label = t.t(SCHEME_LABEL_KEY[option])
|
|
67
|
+
const selected = scheme === option
|
|
68
|
+
return (
|
|
54
69
|
<PendingButton
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
key={option}
|
|
71
|
+
name="scheme"
|
|
72
|
+
value={option}
|
|
73
|
+
title={label}
|
|
74
|
+
aria-pressed={selected}
|
|
75
|
+
className={`inline-flex h-8 items-center justify-center border-border px-2.5 transition-colors [&:not(:first-child)]:border-l focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring ${
|
|
76
|
+
selected
|
|
77
|
+
? 'bg-primary text-primary-foreground'
|
|
78
|
+
: 'bg-background hover:bg-accent hover:text-accent-foreground'
|
|
79
|
+
}`}
|
|
57
80
|
>
|
|
58
|
-
|
|
81
|
+
<Icon />
|
|
82
|
+
<span className="sr-only">{label}</span>
|
|
59
83
|
</PendingButton>
|
|
60
|
-
|
|
61
|
-
)}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
role="group"
|
|
66
|
-
aria-label={t.t('appearance.schemeGroup')}
|
|
67
|
-
className="inline-flex items-center overflow-hidden rounded-md border border-border text-muted-foreground"
|
|
68
|
-
>
|
|
69
|
-
{COLOUR_SCHEMES.map((option) => {
|
|
70
|
-
const Icon = SCHEME_ICON[option]
|
|
71
|
-
const label = t.t(SCHEME_LABEL_KEY[option])
|
|
72
|
-
const selected = scheme === option
|
|
73
|
-
return (
|
|
74
|
-
<PendingButton
|
|
75
|
-
key={option}
|
|
76
|
-
name="scheme"
|
|
77
|
-
value={option}
|
|
78
|
-
title={label}
|
|
79
|
-
aria-pressed={selected}
|
|
80
|
-
className={`inline-flex h-8 items-center justify-center border-border px-2.5 transition-colors [&:not(:first-child)]:border-l focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring ${
|
|
81
|
-
selected
|
|
82
|
-
? 'bg-primary text-primary-foreground'
|
|
83
|
-
: 'bg-background hover:bg-accent hover:text-accent-foreground'
|
|
84
|
-
}`}
|
|
85
|
-
>
|
|
86
|
-
<Icon />
|
|
87
|
-
<span className="sr-only">{label}</span>
|
|
88
|
-
</PendingButton>
|
|
89
|
-
)
|
|
90
|
-
})}
|
|
91
|
-
</span>
|
|
92
|
-
</form>
|
|
93
|
-
</section>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
84
|
+
)
|
|
85
|
+
})}
|
|
86
|
+
</span>
|
|
87
|
+
</form>
|
|
88
|
+
</section>
|
|
96
89
|
)
|
|
97
90
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { cookies } from 'next/headers'
|
|
4
|
+
|
|
5
|
+
import { INSTALL_BANNER_COOKIE, INSTALL_BANNER_COOKIE_MAX_AGE } from '@/view/install-banner'
|
|
6
|
+
|
|
7
|
+
import { redirectToCurrentPath } from './redirect-back'
|
|
8
|
+
|
|
9
|
+
export async function dismissInstallBannerAction(): Promise<void> {
|
|
10
|
+
;(await cookies()).set(INSTALL_BANNER_COOKIE, '1', {
|
|
11
|
+
path: '/',
|
|
12
|
+
maxAge: INSTALL_BANNER_COOKIE_MAX_AGE,
|
|
13
|
+
sameSite: 'lax',
|
|
14
|
+
httpOnly: false,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
await redirectToCurrentPath()
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { cookies } from 'next/headers'
|
|
4
|
+
|
|
5
|
+
import { INSTALL_BANNER_COOKIE } from '@/view/install-banner'
|
|
6
|
+
|
|
7
|
+
import { getTranslator } from './i18n'
|
|
8
|
+
import { getSettings } from './settings'
|
|
9
|
+
|
|
10
|
+
export interface InstallBannerModel {
|
|
11
|
+
readonly message: string
|
|
12
|
+
readonly how: string
|
|
13
|
+
readonly installLabel: string
|
|
14
|
+
readonly dismissLabel: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function installBanner(): Promise<InstallBannerModel | null> {
|
|
18
|
+
const settings = await getSettings()
|
|
19
|
+
if (!settings.get('board.install_banner')) return null
|
|
20
|
+
if ((await cookies()).get(INSTALL_BANNER_COOKIE)?.value === '1') return null
|
|
21
|
+
|
|
22
|
+
const t = await getTranslator()
|
|
23
|
+
return {
|
|
24
|
+
message: t.t('installBanner.message', { board: settings.get('board.name') }),
|
|
25
|
+
how: t.t('installBanner.how'),
|
|
26
|
+
installLabel: t.t('installBanner.install'),
|
|
27
|
+
dismissLabel: t.t('installBanner.dismiss'),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -16,7 +16,7 @@ import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
|
|
|
16
16
|
|
|
17
17
|
import { activeDefinitions } from './plugin-host'
|
|
18
18
|
|
|
19
|
-
export const CODE_VERSION = '0.30.
|
|
19
|
+
export const CODE_VERSION = '0.30.1'
|
|
20
20
|
|
|
21
21
|
export interface UpgradeApplied {
|
|
22
22
|
readonly plugins: readonly string[]
|
|
@@ -180,8 +180,9 @@ export const SLOT_FIXTURES: { readonly [K in SlotName]?: SlotFixture<K> } = {
|
|
|
180
180
|
links: [{ label: 'Contact', href: '/contact' }],
|
|
181
181
|
timezoneLabel: 'Europe/London',
|
|
182
182
|
poweredBy: { label: 'Powered by Meith', href: 'https://meith.dev' },
|
|
183
|
+
regions: { controls: region('footer-controls') },
|
|
183
184
|
},
|
|
184
|
-
requires: ['Europe/London', '/contact'],
|
|
185
|
+
requires: ['Europe/London', '/contact', region('footer-controls')],
|
|
185
186
|
},
|
|
186
187
|
|
|
187
188
|
Notice: {
|