@tuturuuu/ui 0.19.0 → 0.20.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/CHANGELOG.md +46 -0
- package/biome.json +1 -1
- package/package.json +62 -50
- package/src/components/ui/badge.tsx +2 -0
- package/src/components/ui/button.tsx +2 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +48 -4
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +22 -0
- package/src/components/ui/custom/animated-slot-text.tsx +20 -0
- package/src/components/ui/custom/common-footer.tsx +262 -237
- package/src/components/ui/custom/language-dropdown-item.tsx +3 -10
- package/src/components/ui/custom/language-toggle.test.tsx +30 -0
- package/src/components/ui/custom/language-toggle.tsx +11 -10
- package/src/components/ui/custom/locale-preference.ts +32 -0
- package/src/components/ui/custom/settings/appearance-settings.tsx +5 -13
- package/src/components/ui/custom/settings-dialog-shell.tsx +40 -9
- package/src/components/ui/custom/structure.tsx +12 -0
- package/src/components/ui/custom/system-language-dropdown-item.tsx +3 -6
- package/src/components/ui/custom/workspace-access/adapters.test.ts +10 -1
- package/src/components/ui/custom/workspace-access/adapters.ts +36 -4
- package/src/components/ui/custom/workspace-access/member-filter-utils.test.ts +14 -0
- package/src/components/ui/custom/workspace-access/member-filter-utils.ts +8 -0
- package/src/components/ui/custom/workspace-access/types.ts +20 -0
- package/src/components/ui/custom/workspace-access/workspace-access-context.test.tsx +111 -0
- package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +15 -5
- package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +155 -48
- package/src/components/ui/custom/workspace-access/workspace-access-member-profile-dialog.tsx +92 -0
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +171 -76
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +11 -2
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +11 -11
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +180 -22
- package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +29 -15
- package/src/components/ui/custom/workspace-access/workspace-access-permission-checklist.tsx +56 -10
- package/src/components/ui/custom/workspace-access/workspace-access-permission-preview.test.ts +33 -0
- package/src/components/ui/custom/workspace-access/workspace-access-permission-preview.tsx +24 -1
- package/src/components/ui/custom/workspace-access/workspace-access-responsive.test.ts +64 -0
- package/src/components/ui/custom/workspace-access/workspace-access-role-editor-dialog.tsx +28 -16
- package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +35 -11
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +23 -11
- package/src/components/ui/custom/workspace-select-helpers.ts +5 -3
- package/src/components/ui/custom/workspace-select.tsx +8 -2
- package/src/components/ui/finance/shared/charts/monthly-total-chart-client.tsx +1 -1
- package/src/components/ui/finance/shared/charts/monthly-total-chart.tsx +1 -1
- package/src/components/ui/storefront/cart-summary.tsx +12 -2
- package/src/components/ui/storefront/storefront-surface.test.tsx +21 -0
- package/src/components/ui/storefront/storefront-surface.tsx +6 -0
- package/src/components/ui/text-editor/__tests__/collaboration-binding.test.tsx +161 -0
- package/src/components/ui/text-editor/editor.tsx +267 -235
- package/src/globals.css +166 -0
- package/src/hooks/__tests__/use-workspace-identity-mutation.test.tsx +181 -0
- package/src/hooks/use-workspace-identity-mutation.ts +136 -0
- package/vendor/xlsx/LICENSE +201 -0
- package/vendor/xlsx/types/index.d.ts +1065 -0
- package/vendor/xlsx/xlsx.js +28103 -0
- package/vendor/xlsx/xlsx.mjs +28228 -0
- package/vendor/xlsx-0.20.3.tgz +0 -0
|
@@ -1,8 +1,70 @@
|
|
|
1
1
|
import { getLocalInternalAppUrl } from '@tuturuuu/utils/internal-domains';
|
|
2
2
|
import Link from 'next/link';
|
|
3
|
-
import {
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
4
|
import { TuturuuLogo } from './tuturuuu-logo';
|
|
5
5
|
|
|
6
|
+
interface FooterLink {
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
href: string;
|
|
9
|
+
external?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function FooterColumn({
|
|
13
|
+
title,
|
|
14
|
+
links,
|
|
15
|
+
}: {
|
|
16
|
+
title: ReactNode;
|
|
17
|
+
links: FooterLink[];
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="grid content-start gap-3">
|
|
21
|
+
<div className="flex items-center gap-2">
|
|
22
|
+
<span className="font-mono-ui text-[0.6rem] text-foreground/35 uppercase tracking-[0.2em]">
|
|
23
|
+
{title}
|
|
24
|
+
</span>
|
|
25
|
+
<span
|
|
26
|
+
aria-hidden
|
|
27
|
+
className="h-px flex-1 bg-gradient-to-r from-foreground/12 to-transparent"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
{links.map((link) => (
|
|
31
|
+
<Link
|
|
32
|
+
className="w-fit text-foreground/60 text-sm transition-colors duration-200 hover:text-foreground"
|
|
33
|
+
href={link.href}
|
|
34
|
+
key={link.href}
|
|
35
|
+
{...(link.external
|
|
36
|
+
? { target: '_blank', rel: 'noopener noreferrer' }
|
|
37
|
+
: {})}
|
|
38
|
+
>
|
|
39
|
+
{link.label}
|
|
40
|
+
</Link>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function SocialLink({
|
|
47
|
+
href,
|
|
48
|
+
label,
|
|
49
|
+
children,
|
|
50
|
+
}: {
|
|
51
|
+
href: string;
|
|
52
|
+
label: string;
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<Link
|
|
57
|
+
aria-label={label}
|
|
58
|
+
className="flex h-9 w-9 items-center justify-center rounded-lg border border-foreground/10 fill-foreground/50 transition-colors duration-300 hover:border-foreground/25 hover:fill-foreground"
|
|
59
|
+
href={href}
|
|
60
|
+
rel="noopener noreferrer"
|
|
61
|
+
target="_blank"
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</Link>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
6
68
|
export function CommonFooter({
|
|
7
69
|
t,
|
|
8
70
|
devMode,
|
|
@@ -15,266 +77,229 @@ export function CommonFooter({
|
|
|
15
77
|
const TUTURUUU_URL = devMode
|
|
16
78
|
? getLocalInternalAppUrl('platform', 'http://localhost:7803')
|
|
17
79
|
: 'https://tuturuuu.com';
|
|
80
|
+
// The standalone qr.tuturuuu.com host is retired; the generator now lives
|
|
81
|
+
// under the tools app.
|
|
18
82
|
const QR_URL = devMode
|
|
19
|
-
? getLocalInternalAppUrl('
|
|
20
|
-
: 'https://
|
|
83
|
+
? `${getLocalInternalAppUrl('tools', 'http://localhost:7825')}/qr`
|
|
84
|
+
: 'https://tools.tuturuuu.com/qr';
|
|
21
85
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
86
|
+
const columns: Array<{ title: ReactNode; links: FooterLink[] }> = [
|
|
87
|
+
{
|
|
88
|
+
title: t('common.resources'),
|
|
89
|
+
links: [
|
|
90
|
+
{ label: t('common.blog'), href: `${TUTURUUU_URL}/blog` },
|
|
91
|
+
{
|
|
92
|
+
label: t('common.meet-together'),
|
|
93
|
+
href: `${TUTURUUU_URL}/meet-together`,
|
|
94
|
+
},
|
|
95
|
+
{ label: t('common.qr_generator'), href: QR_URL },
|
|
96
|
+
{
|
|
97
|
+
label: t('common.facebook_mockup'),
|
|
98
|
+
href: `${TUTURUUU_URL}/facebook-mockup`,
|
|
99
|
+
},
|
|
100
|
+
{ label: t('common.branding'), href: `${TUTURUUU_URL}/branding` },
|
|
101
|
+
{ label: t('common.ui'), href: `${TUTURUUU_URL}/ui` },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
title: t('common.company'),
|
|
106
|
+
links: [
|
|
107
|
+
{ label: t('common.about'), href: `${TUTURUUU_URL}/about` },
|
|
108
|
+
{ label: t('common.partners'), href: `${TUTURUUU_URL}/partners` },
|
|
109
|
+
{ label: t('common.careers'), href: `${TUTURUUU_URL}/careers` },
|
|
110
|
+
{ label: t('common.contact'), href: `${TUTURUUU_URL}/contact` },
|
|
111
|
+
{ label: t('common.pricing'), href: `${TUTURUUU_URL}/pricing` },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
title: t('common.legal'),
|
|
116
|
+
links: [
|
|
117
|
+
{ label: t('common.security'), href: `${TUTURUUU_URL}/security` },
|
|
118
|
+
{ label: t('common.terms'), href: `${TUTURUUU_URL}/terms` },
|
|
119
|
+
{ label: t('common.privacy'), href: `${TUTURUUU_URL}/privacy` },
|
|
120
|
+
{
|
|
121
|
+
label: t('common.community-guidelines'),
|
|
122
|
+
href: `${TUTURUUU_URL}/community-guidelines`,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
label: t('common.acceptable-use'),
|
|
126
|
+
href: `${TUTURUUU_URL}/acceptable-use`,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
title: t('common.developers'),
|
|
132
|
+
links: [
|
|
133
|
+
{
|
|
134
|
+
label: t('common.documentation'),
|
|
135
|
+
href: 'https://docs.tuturuuu.com',
|
|
136
|
+
external: true,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
label: t('common.open-source'),
|
|
140
|
+
href: 'https://github.com/tutur3u/platform',
|
|
141
|
+
external: true,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
label: t('common.changelog'),
|
|
145
|
+
href: `${TUTURUUU_URL}/changelog`,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
];
|
|
32
150
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
aria-label="Tuturuuu"
|
|
41
|
-
>
|
|
42
|
-
<TuturuuLogo
|
|
43
|
-
width={64}
|
|
44
|
-
height={64}
|
|
45
|
-
alt="logo"
|
|
46
|
-
src={logoSrc}
|
|
47
|
-
className="h-12 w-12"
|
|
48
|
-
/>
|
|
49
|
-
<div className="font-semibold text-4xl">Tuturuuu</div>
|
|
50
|
-
</Link>
|
|
151
|
+
return (
|
|
152
|
+
<footer className="relative mt-16 w-full border-foreground/10 border-t">
|
|
153
|
+
{/* Brand wash */}
|
|
154
|
+
<div
|
|
155
|
+
aria-hidden
|
|
156
|
+
className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-dynamic-purple/40 to-transparent"
|
|
157
|
+
/>
|
|
51
158
|
|
|
52
|
-
|
|
159
|
+
<div className="mx-auto w-full max-w-7xl px-6 py-14 lg:px-8">
|
|
160
|
+
<div className="grid gap-12 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]">
|
|
161
|
+
{/* Brand */}
|
|
162
|
+
<div className="grid content-start gap-5">
|
|
53
163
|
<Link
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
aria-label="Facebook"
|
|
164
|
+
aria-label="Tuturuuu"
|
|
165
|
+
className="flex w-fit items-center gap-3 transition-opacity hover:opacity-80"
|
|
166
|
+
href={TUTURUUU_URL}
|
|
58
167
|
>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
168
|
+
<TuturuuLogo
|
|
169
|
+
alt="logo"
|
|
170
|
+
className="h-9 w-9"
|
|
171
|
+
height={48}
|
|
172
|
+
src={logoSrc}
|
|
173
|
+
width={48}
|
|
174
|
+
/>
|
|
175
|
+
<span className="font-display font-semibold text-2xl tracking-[-0.03em]">
|
|
176
|
+
Tuturuuu
|
|
177
|
+
</span>
|
|
67
178
|
</Link>
|
|
68
179
|
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
className="fill-foreground/50 transition duration-300 hover:fill-foreground"
|
|
73
|
-
aria-label="Instagram"
|
|
74
|
-
>
|
|
75
|
-
<svg
|
|
76
|
-
viewBox="0 0 32 32"
|
|
77
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
78
|
-
className="w-6 scale-150"
|
|
79
|
-
>
|
|
80
|
-
<title>Instagram</title>
|
|
81
|
-
<path d="M20.445 5h-8.891A6.559 6.559 0 0 0 5 11.554v8.891A6.559 6.559 0 0 0 11.554 27h8.891a6.56 6.56 0 0 0 6.554-6.555v-8.891A6.557 6.557 0 0 0 20.445 5zm4.342 15.445a4.343 4.343 0 0 1-4.342 4.342h-8.891a4.341 4.341 0 0 1-4.341-4.342v-8.891a4.34 4.34 0 0 1 4.341-4.341h8.891a4.342 4.342 0 0 1 4.341 4.341l.001 8.891z" />
|
|
82
|
-
<path d="M16 10.312c-3.138 0-5.688 2.551-5.688 5.688s2.551 5.688 5.688 5.688 5.688-2.551 5.688-5.688-2.55-5.688-5.688-5.688zm0 9.163a3.475 3.475 0 1 1-.001-6.95 3.475 3.475 0 0 1 .001 6.95zM21.7 8.991a1.363 1.363 0 1 1-1.364 1.364c0-.752.51-1.364 1.364-1.364z" />
|
|
83
|
-
</svg>
|
|
84
|
-
</Link>
|
|
180
|
+
<p className="max-w-xs text-balance text-foreground/60 text-sm leading-relaxed">
|
|
181
|
+
{t('common.footer_tagline')}
|
|
182
|
+
</p>
|
|
85
183
|
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
aria-label="X (formerly Twitter)"
|
|
91
|
-
>
|
|
92
|
-
<svg
|
|
93
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
94
|
-
viewBox="0 0 42 42"
|
|
95
|
-
className="w-6"
|
|
184
|
+
<div className="flex flex-wrap gap-2">
|
|
185
|
+
<SocialLink
|
|
186
|
+
href="https://www.facebook.com/tuturuuu"
|
|
187
|
+
label="Facebook"
|
|
96
188
|
>
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</svg>
|
|
107
|
-
</Link>
|
|
189
|
+
<svg
|
|
190
|
+
className="w-4"
|
|
191
|
+
viewBox="0 0 512 512"
|
|
192
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
193
|
+
>
|
|
194
|
+
<title>Facebook</title>
|
|
195
|
+
<path d="M504 256C504 119 393 8 256 8S8 119 8 256c0 123.78 90.69 226.38 209.25 245V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.28c-30.8 0-40.41 19.12-40.41 38.73V256h68.78l-11 71.69h-57.78V501C413.31 482.38 504 379.78 504 256z" />
|
|
196
|
+
</svg>
|
|
197
|
+
</SocialLink>
|
|
108
198
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
className="fill-foreground/50 transition duration-300 hover:fill-foreground"
|
|
113
|
-
aria-label="Github"
|
|
114
|
-
>
|
|
115
|
-
<svg
|
|
116
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
117
|
-
viewBox="0 0 512 512"
|
|
118
|
-
className="w-6"
|
|
199
|
+
<SocialLink
|
|
200
|
+
href="https://www.instagram.com/tutu.ruuu/"
|
|
201
|
+
label="Instagram"
|
|
119
202
|
>
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
203
|
+
<svg
|
|
204
|
+
className="w-5"
|
|
205
|
+
viewBox="0 0 32 32"
|
|
206
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
207
|
+
>
|
|
208
|
+
<title>Instagram</title>
|
|
209
|
+
<path d="M20.445 5h-8.891A6.559 6.559 0 0 0 5 11.554v8.891A6.559 6.559 0 0 0 11.554 27h8.891a6.56 6.56 0 0 0 6.554-6.555v-8.891A6.557 6.557 0 0 0 20.445 5zm4.342 15.445a4.343 4.343 0 0 1-4.342 4.342h-8.891a4.341 4.341 0 0 1-4.341-4.342v-8.891a4.34 4.34 0 0 1 4.341-4.341h8.891a4.342 4.342 0 0 1 4.341 4.341l.001 8.891z" />
|
|
210
|
+
<path d="M16 10.312c-3.138 0-5.688 2.551-5.688 5.688s2.551 5.688 5.688 5.688 5.688-2.551 5.688-5.688-2.55-5.688-5.688-5.688zm0 9.163a3.475 3.475 0 1 1-.001-6.95 3.475 3.475 0 0 1 .001 6.95zM21.7 8.991a1.363 1.363 0 1 1-1.364 1.364c0-.752.51-1.364 1.364-1.364z" />
|
|
211
|
+
</svg>
|
|
212
|
+
</SocialLink>
|
|
124
213
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
className="fill-foreground/50 transition duration-300 hover:fill-foreground"
|
|
129
|
-
aria-label="LinkedIn"
|
|
130
|
-
>
|
|
131
|
-
<svg
|
|
132
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
133
|
-
viewBox="0 0 512 512"
|
|
134
|
-
className="w-6"
|
|
214
|
+
<SocialLink
|
|
215
|
+
href="https://x.com/tutur3u"
|
|
216
|
+
label="X (formerly Twitter)"
|
|
135
217
|
>
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
218
|
+
<svg
|
|
219
|
+
className="w-4"
|
|
220
|
+
viewBox="0 0 42 42"
|
|
221
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
222
|
+
>
|
|
223
|
+
<title>X (formerly Twitter)</title>
|
|
224
|
+
<polygon points="41,6 9.929,42 6.215,42 37.287,6" />
|
|
225
|
+
<polygon
|
|
226
|
+
className="fill-background"
|
|
227
|
+
clipRule="evenodd"
|
|
228
|
+
fillRule="evenodd"
|
|
229
|
+
points="31.143,41 7.82,7 16.777,7 40.1,41"
|
|
230
|
+
/>
|
|
231
|
+
<path d="M15.724,9l20.578,30h-4.106L11.618,9H15.724 M17.304,6H5.922l24.694,36h11.382L17.304,6L17.304,6z" />
|
|
232
|
+
</svg>
|
|
233
|
+
</SocialLink>
|
|
142
234
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
</Link>
|
|
154
|
-
<Link
|
|
155
|
-
href={`${TUTURUUU_URL}/meet-together`}
|
|
156
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
157
|
-
>
|
|
158
|
-
{t('common.meet-together')}
|
|
159
|
-
</Link>
|
|
160
|
-
<Link
|
|
161
|
-
href={QR_URL}
|
|
162
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
163
|
-
>
|
|
164
|
-
{t('common.qr_generator')}
|
|
165
|
-
</Link>
|
|
166
|
-
<Link
|
|
167
|
-
href={`${TUTURUUU_URL}/facebook-mockup`}
|
|
168
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
169
|
-
>
|
|
170
|
-
{t('common.facebook_mockup')}
|
|
171
|
-
</Link>
|
|
172
|
-
<Link
|
|
173
|
-
href={`${TUTURUUU_URL}/branding`}
|
|
174
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
175
|
-
>
|
|
176
|
-
{t('common.branding')}
|
|
177
|
-
</Link>
|
|
178
|
-
<Link
|
|
179
|
-
href={`${TUTURUUU_URL}/ui`}
|
|
180
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
181
|
-
>
|
|
182
|
-
{t('common.ui')}
|
|
183
|
-
</Link>
|
|
184
|
-
</div>
|
|
235
|
+
<SocialLink href="https://github.com/tutur3u" label="Github">
|
|
236
|
+
<svg
|
|
237
|
+
className="w-4"
|
|
238
|
+
viewBox="0 0 512 512"
|
|
239
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
240
|
+
>
|
|
241
|
+
<title>GitHub</title>
|
|
242
|
+
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
|
|
243
|
+
</svg>
|
|
244
|
+
</SocialLink>
|
|
185
245
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
246
|
+
<SocialLink
|
|
247
|
+
href="https://www.linkedin.com/company/tuturuuu/"
|
|
248
|
+
label="LinkedIn"
|
|
249
|
+
>
|
|
250
|
+
<svg
|
|
251
|
+
className="w-4"
|
|
252
|
+
viewBox="0 0 512 512"
|
|
253
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
254
|
+
>
|
|
255
|
+
<title>LinkedIn</title>
|
|
256
|
+
<path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z" />
|
|
257
|
+
</svg>
|
|
258
|
+
</SocialLink>
|
|
189
259
|
</div>
|
|
190
|
-
<Link
|
|
191
|
-
href={`${TUTURUUU_URL}/about`}
|
|
192
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
193
|
-
>
|
|
194
|
-
{t('common.about')}
|
|
195
|
-
</Link>
|
|
196
|
-
<Link
|
|
197
|
-
href={`${TUTURUUU_URL}/partners`}
|
|
198
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
199
|
-
>
|
|
200
|
-
{t('common.partners')}
|
|
201
|
-
</Link>
|
|
202
|
-
<Link
|
|
203
|
-
href={`${TUTURUUU_URL}/contact`}
|
|
204
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
205
|
-
>
|
|
206
|
-
{t('common.contact')}
|
|
207
|
-
</Link>
|
|
208
|
-
<Link
|
|
209
|
-
href={`${TUTURUUU_URL}/pricing`}
|
|
210
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
211
|
-
>
|
|
212
|
-
{t('common.pricing')}
|
|
213
|
-
</Link>
|
|
214
260
|
</div>
|
|
215
261
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
</Link>
|
|
226
|
-
<Link
|
|
227
|
-
href={`${TUTURUUU_URL}/terms`}
|
|
228
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
229
|
-
>
|
|
230
|
-
{t('common.terms')}
|
|
231
|
-
</Link>
|
|
232
|
-
<Link
|
|
233
|
-
href={`${TUTURUUU_URL}/privacy`}
|
|
234
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
235
|
-
>
|
|
236
|
-
{t('common.privacy')}
|
|
237
|
-
</Link>
|
|
238
|
-
<Link
|
|
239
|
-
href={`${TUTURUUU_URL}/community-guidelines`}
|
|
240
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
241
|
-
>
|
|
242
|
-
{t('common.community-guidelines')}
|
|
243
|
-
</Link>
|
|
244
|
-
<Link
|
|
245
|
-
href={`${TUTURUUU_URL}/acceptable-use`}
|
|
246
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
247
|
-
>
|
|
248
|
-
{t('common.acceptable-use')}
|
|
249
|
-
</Link>
|
|
262
|
+
{/* Link columns */}
|
|
263
|
+
<div className="grid grid-cols-2 gap-8 sm:grid-cols-4">
|
|
264
|
+
{columns.map((column) => (
|
|
265
|
+
<FooterColumn
|
|
266
|
+
key={String(column.title)}
|
|
267
|
+
links={column.links}
|
|
268
|
+
title={column.title}
|
|
269
|
+
/>
|
|
270
|
+
))}
|
|
250
271
|
</div>
|
|
272
|
+
</div>
|
|
251
273
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
className="text-foreground/80 text-sm hover:text-foreground hover:underline md:w-fit"
|
|
267
|
-
>
|
|
268
|
-
{t('common.open-source')}
|
|
269
|
-
</Link>
|
|
270
|
-
</div>
|
|
274
|
+
{/* Bottom bar */}
|
|
275
|
+
<div className="mt-14 flex flex-col items-center justify-between gap-4 border-foreground/10 border-t pt-8 sm:flex-row">
|
|
276
|
+
<p className="text-balance text-center font-mono-ui text-[0.65rem] text-foreground/35 uppercase tracking-[0.12em] sm:text-left">
|
|
277
|
+
{t('common.copyright')}
|
|
278
|
+
</p>
|
|
279
|
+
<Link
|
|
280
|
+
className="inline-flex items-center gap-2 rounded-full border border-foreground/10 px-3 py-1.5 font-mono-ui text-[0.62rem] text-foreground/50 uppercase tracking-[0.16em] transition-colors hover:border-foreground/25 hover:text-foreground"
|
|
281
|
+
href="https://github.com/tutur3u/platform"
|
|
282
|
+
rel="noopener noreferrer"
|
|
283
|
+
target="_blank"
|
|
284
|
+
>
|
|
285
|
+
<span className="h-1.5 w-1.5 rounded-full bg-dynamic-green" />
|
|
286
|
+
{t('common.open-source')}
|
|
287
|
+
</Link>
|
|
271
288
|
</div>
|
|
272
289
|
</div>
|
|
273
290
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
291
|
+
{/* Wordmark finale — the page signs off at full width, clipped by the
|
|
292
|
+
viewport edge so it reads as a mark rather than a heading. */}
|
|
293
|
+
<div
|
|
294
|
+
aria-hidden
|
|
295
|
+
className="relative select-none overflow-hidden px-6 pb-2 lg:px-8"
|
|
296
|
+
>
|
|
297
|
+
<div className="mx-auto max-w-7xl">
|
|
298
|
+
<div className="bg-[linear-gradient(180deg,color-mix(in_oklab,var(--foreground)_11%,transparent),transparent)] bg-clip-text text-center font-display font-extrabold text-[18vw] text-transparent leading-[0.78] tracking-[-0.055em]">
|
|
299
|
+
Tuturuuu
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
277
302
|
</div>
|
|
278
|
-
</
|
|
303
|
+
</footer>
|
|
279
304
|
);
|
|
280
305
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Check, Hexagon } from '@tuturuuu/icons';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
5
|
import { DropdownMenuItem } from '../dropdown-menu';
|
|
6
|
+
import { persistLocalePreference } from './locale-preference';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
label: string;
|
|
@@ -26,16 +27,8 @@ export function LanguageDropdownItem({
|
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
method: 'POST',
|
|
32
|
-
body: JSON.stringify({ locale }),
|
|
33
|
-
headers: {
|
|
34
|
-
'Content-Type': 'application/json',
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
if (res.ok) router.refresh();
|
|
30
|
+
persistLocalePreference(locale);
|
|
31
|
+
router.refresh();
|
|
39
32
|
};
|
|
40
33
|
|
|
41
34
|
return (
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { LanguageToggle } from './language-toggle';
|
|
4
|
+
|
|
5
|
+
const router = vi.hoisted(() => ({ refresh: vi.fn() }));
|
|
6
|
+
|
|
7
|
+
vi.mock('next/navigation', () => ({
|
|
8
|
+
useRouter: () => router,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe('LanguageToggle', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
router.refresh.mockReset();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('refreshes after persisting the next locale without an app API route', () => {
|
|
18
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
19
|
+
|
|
20
|
+
render(<LanguageToggle currentLocale="en" forceDisplay />);
|
|
21
|
+
fireEvent.click(screen.getByRole('button', { name: 'enToggle language' }));
|
|
22
|
+
|
|
23
|
+
expect(router.refresh).toHaveBeenCalledTimes(1);
|
|
24
|
+
expect(document.cookie).toContain('NEXT_LOCALE=vi');
|
|
25
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
26
|
+
expect(
|
|
27
|
+
screen.getByRole('button', { name: 'enToggle language' })
|
|
28
|
+
).toBeEnabled();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation';
|
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { Button } from '../button';
|
|
6
6
|
import { LoadingIndicator } from './loading-indicator';
|
|
7
|
+
import { persistLocalePreference } from './locale-preference';
|
|
7
8
|
|
|
8
9
|
export function LanguageToggle({
|
|
9
10
|
forceDisplay = false,
|
|
@@ -15,18 +16,17 @@ export function LanguageToggle({
|
|
|
15
16
|
const [loading, setLoading] = useState(false);
|
|
16
17
|
const router = useRouter();
|
|
17
18
|
|
|
18
|
-
const updateLocale =
|
|
19
|
+
const updateLocale = () => {
|
|
19
20
|
setLoading(true);
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (res.ok) router.refresh();
|
|
22
|
+
try {
|
|
23
|
+
persistLocalePreference(currentLocale === 'en' ? 'vi' : 'en');
|
|
24
|
+
router.refresh();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Failed to update locale', error);
|
|
27
|
+
} finally {
|
|
28
|
+
setLoading(false);
|
|
29
|
+
}
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
return (
|
|
@@ -34,6 +34,7 @@ export function LanguageToggle({
|
|
|
34
34
|
variant="outline"
|
|
35
35
|
size="icon"
|
|
36
36
|
onClick={updateLocale}
|
|
37
|
+
disabled={loading}
|
|
37
38
|
className={forceDisplay ? 'flex-none' : 'hidden flex-none md:flex'}
|
|
38
39
|
>
|
|
39
40
|
{loading ? <LoadingIndicator /> : currentLocale}
|