122 |
123 |
124 |
125 | Extract
126 |
127 |
128 |
129 |
130 |
131 |
{localeData.title}
132 |
133 | {localeData.description}
134 |
135 |
136 |
137 |
148 | {!submitting && actionData?.error && (
149 |
150 |
151 | {localeData.errorTitle}
152 | {actionData.error.message}
153 |
154 | )}
155 |
156 | {!submitting && actionData?.images && actionData.images.length > 0 && (
157 |
158 |
159 | {actionData.images.map((image) => (
160 |
165 | ))}
166 |
167 |
168 | )}
169 | {!submitting && actionData?.images && actionData.images.length == 0 && (
170 |
171 |
172 | {localeData.noImages}
173 |
174 |
175 |
176 | )}
177 |
178 |
179 | {(!actionData?.images ||
180 | actionData.images.length == 0 ||
181 | submitting) &&
182 | localeData.features.map((feature, idx) => (
183 |
184 | {featureIcons[idx]}
185 |
186 | {feature.title}
187 | {feature.description}
188 |
189 |
190 | ))}
191 |
192 |
193 |
201 |
202 | );
203 | }
204 |
--------------------------------------------------------------------------------
/app/routes/api.userLang.ts:
--------------------------------------------------------------------------------
1 | import { ActionFunctionArgs, redirect } from "@remix-run/cloudflare";
2 | import { sessionWraper } from "~/sessions.server";
3 |
4 | export async function action({ request, context, params }: ActionFunctionArgs) {
5 | const formData = await request.formData();
6 | const lang = formData.get("lang") as string;
7 | if (!lang || (lang !== "en" && lang !== "zh")) {
8 | return null;
9 | }
10 | const { KV } = context.cloudflare.env;
11 | const { getSession, commitSession } = sessionWraper(KV);
12 | const session = await getSession(request.headers.get("Cookie"));
13 | session.set("lang", lang);
14 | await commitSession(session);
15 | if (lang === "en") {
16 | return redirect("/");
17 | }
18 | return redirect(`/${lang}`);
19 | }
20 |
--------------------------------------------------------------------------------
/app/sessions.server.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createCookie,
3 | createWorkersKVSessionStorage,
4 | } from "@remix-run/cloudflare";
5 |
6 | // In this example the Cookie is created separately.
7 | export const sessionCookie = createCookie("__session", {
8 | secrets: ["r3m1xr0ck5"],
9 | sameSite: true,
10 | });
11 |
12 | export function sessionWraper(kv: KVNamespace) {
13 | return createWorkersKVSessionStorage({
14 | // The KV Namespace where you want to store sessions
15 | kv: kv,
16 | cookie: sessionCookie,
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/app/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 240 10% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 240 10% 3.9%;
15 |
16 | --primary: 240 5.9% 10%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 240 4.8% 95.9%;
20 | --secondary-foreground: 240 5.9% 10%;
21 |
22 | --muted: 240 4.8% 95.9%;
23 | --muted-foreground: 240 3.8% 46.1%;
24 |
25 | --accent: 240 4.8% 95.9%;
26 | --accent-foreground: 240 5.9% 10%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 240 5.9% 90%;
32 | --input: 240 5.9% 90%;
33 | --ring: 240 10% 3.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 240 10% 3.9%;
40 | --foreground: 0 0% 98%;
41 |
42 | --card: 240 10% 3.9%;
43 | --card-foreground: 0 0% 98%;
44 |
45 | --popover: 240 10% 3.9%;
46 | --popover-foreground: 0 0% 98%;
47 |
48 | --primary: 0 0% 98%;
49 | --primary-foreground: 240 5.9% 10%;
50 |
51 | --secondary: 240 3.7% 15.9%;
52 | --secondary-foreground: 0 0% 98%;
53 |
54 | --muted: 240 3.7% 15.9%;
55 | --muted-foreground: 240 5% 64.9%;
56 |
57 | --accent: 240 3.7% 15.9%;
58 | --accent-foreground: 0 0% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 0 0% 98%;
62 |
63 | --border: 240 3.7% 15.9%;
64 | --input: 240 3.7% 15.9%;
65 | --ring: 240 4.9% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/types.ts:
--------------------------------------------------------------------------------
1 | export interface ExtractImageData {
2 | src: string;
3 | size: number;
4 | mimeType: string;
5 | width: number;
6 | height: number;
7 | decoded: boolean;
8 | }
9 |
--------------------------------------------------------------------------------
/app/ui/primitives/utils.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext as React_createContext,
3 | useCallback,
4 | useContext,
5 | useEffect,
6 | useLayoutEffect as React_useLayoutEffect,
7 | useMemo,
8 | useState,
9 | } from "react";
10 |
11 | export const canUseDOM = !!(
12 | typeof window !== "undefined" &&
13 | window.document &&
14 | window.document.createElement
15 | );
16 |
17 | export const useLayoutEffect = canUseDOM ? React_useLayoutEffect : useEffect;
18 |
19 | let hydrating = true;
20 | export function useHydrated() {
21 | let [hydrated, setHydrated] = useState(() => !hydrating);
22 | useEffect(() => {
23 | hydrating = false;
24 | setHydrated(true);
25 | }, []);
26 | return hydrated;
27 | }
28 |
29 | export function useForceUpdate() {
30 | let [, dispatch] = useState(() => Object.create(null));
31 | return useCallback(() => {
32 | dispatch(Object.create(null));
33 | }, []);
34 | }
35 |
36 | export function composeEventHandlers<
37 | EventType extends { defaultPrevented: boolean },
38 | >(
39 | theirHandler: ((event: EventType) => any) | undefined,
40 | ourHandler: (event: EventType) => any,
41 | ): (event: EventType) => any {
42 | return (event) => {
43 | theirHandler && theirHandler(event);
44 | if (!event.defaultPrevented) {
45 | return ourHandler(event);
46 | }
47 | };
48 | }
49 |
50 | /**
51 | * React.Ref uses the readonly type `React.RefObject` instead of
52 | * `React.MutableRefObject`, We pretty much always assume ref objects are
53 | * mutable (at least when we create them), so this type is a workaround so some
54 | * of the weird mechanics of using refs with TS.
55 | */
56 | export type AssignableRef