diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..abfdc28
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+VITE_SUPABASE_URL=
+VITE_SUPABASE_ANON_KEY=
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index a4b7629..8adc462 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup | Node.js
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: 20
# node-version-file: .nvmrc
diff --git a/README.md b/README.md
index abff58e..81797a2 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ Mac や WSL では [mise](https://mise.jdx.dev/getting-started.html) 等を経
```sh
npm install
```
+`.env.example` ファイルをコピーして `.env` ファイルを作成し、環境変数を設定してください。(イコールの間に空白を入れないよう注意)
## 開発
diff --git a/app/components/Loading.tsx b/app/components/Loading.tsx
new file mode 100644
index 0000000..77705a9
--- /dev/null
+++ b/app/components/Loading.tsx
@@ -0,0 +1,7 @@
+import type { ReactNode } from "react";
+
+export const Loading = (): ReactNode => (
+
+);
diff --git a/app/hooks/useSession.ts b/app/hooks/useSession.ts
new file mode 100644
index 0000000..d02ea15
--- /dev/null
+++ b/app/hooks/useSession.ts
@@ -0,0 +1,41 @@
+import type { Session } from "@supabase/supabase-js";
+import useSWRImmutable from "swr/immutable";
+import useSWRSubscription from "swr/subscription";
+import type { SWRSubscriptionOptions } from "swr/subscription";
+import { supabase } from "~/libs/supabase";
+
+export function useSession(): Session | null {
+ const key = "session";
+ const { data: initialSession } = useSWRImmutable(
+ key,
+ async () => {
+ const {
+ data: { session },
+ } = await supabase.auth.getSession();
+
+ return session;
+ },
+ {
+ suspense: true,
+ },
+ );
+
+ const { data: session } = useSWRSubscription(
+ key,
+ (_, { next }: SWRSubscriptionOptions) => {
+ const {
+ data: { subscription },
+ } = supabase.auth.onAuthStateChange((_event, session) => {
+ next(null, session);
+ });
+
+ return subscription.unsubscribe;
+ },
+ {
+ suspense: true,
+ fallbackData: initialSession,
+ },
+ );
+
+ return session ?? null;
+}
diff --git a/app/libs/getAccountCredentials.ts b/app/libs/getAccountCredentials.ts
new file mode 100644
index 0000000..5933964
--- /dev/null
+++ b/app/libs/getAccountCredentials.ts
@@ -0,0 +1,11 @@
+import type { SignInWithPasswordCredentials } from "@supabase/supabase-js";
+
+export function getAccountCredentials(
+ id: string,
+): SignInWithPasswordCredentials {
+ return {
+ // supabase は userId のみでサインインできないため、強引にやる
+ email: `email-${id}@example.com`,
+ password: "password",
+ };
+}
diff --git a/app/libs/supabase.ts b/app/libs/supabase.ts
new file mode 100644
index 0000000..e5bd544
--- /dev/null
+++ b/app/libs/supabase.ts
@@ -0,0 +1,6 @@
+import { createClient } from "@supabase/supabase-js";
+
+export const supabase = createClient(
+ import.meta.env.VITE_SUPABASE_URL,
+ import.meta.env.VITE_SUPABASE_ANON_KEY,
+);
diff --git a/app/root.tsx b/app/root.tsx
index 7f66a39..6fd6ddc 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -6,6 +6,8 @@ import {
ScrollRestoration,
} from "@remix-run/react";
import "./tailwind.css";
+import { Suspense } from "react";
+import { Loading } from "./components/Loading";
export function Layout({ children }: { children: React.ReactNode }) {
return (
@@ -26,5 +28,9 @@ export function Layout({ children }: { children: React.ReactNode }) {
}
export default function App() {
- return ;
+ return (
+ }>
+
+
+ );
}
diff --git a/app/routes/_auth._index.tsx b/app/routes/_auth._index.tsx
new file mode 100644
index 0000000..02f871c
--- /dev/null
+++ b/app/routes/_auth._index.tsx
@@ -0,0 +1,43 @@
+import type { MetaFunction } from "@remix-run/node";
+import { useSession } from "~/hooks/useSession";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "New Remix App" },
+ { name: "description", content: "Welcome to Remix!" },
+ ];
+};
+
+export default function Index() {
+ const session = useSession();
+
+ // 未サインインの場合
+ if (!session)
+ return (
+
+ Game
+
+
+ );
+
+ // サインイン済みの場合
+ return (
+
+ Game
+
+ Hello World
+
+
+ ID: {session.user.email?.replace("@example.com", "")}
+
+
+ );
+}
diff --git a/app/routes/_auth.tsx b/app/routes/_auth.tsx
new file mode 100644
index 0000000..996feb4
--- /dev/null
+++ b/app/routes/_auth.tsx
@@ -0,0 +1,17 @@
+import { Outlet, useNavigate } from "@remix-run/react";
+import { type ReactNode, useLayoutEffect } from "react";
+import { useSession } from "~/hooks/useSession";
+
+// _auth.**.tsx のパスへのアクセスは必ずここで前処理される
+export default function Layout(): ReactNode {
+ const navigate = useNavigate();
+ const session = useSession();
+
+ useLayoutEffect(() => {
+ if (!session) {
+ navigate("/signup");
+ }
+ }, [session, navigate]);
+
+ return ;
+}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
deleted file mode 100644
index 76d0055..0000000
--- a/app/routes/_index.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import type { MetaFunction } from "@remix-run/node";
-
-export const meta: MetaFunction = () => {
- return [
- { title: "New Remix App" },
- { name: "description", content: "Welcome to Remix!" },
- ];
-};
-
-export default function Index() {
- return (
-
- );
-}
diff --git a/app/routes/_noauth.signin.tsx b/app/routes/_noauth.signin.tsx
new file mode 100644
index 0000000..c3f8269
--- /dev/null
+++ b/app/routes/_noauth.signin.tsx
@@ -0,0 +1,55 @@
+import { useNavigate } from "@remix-run/react";
+import type { ReactNode } from "react";
+import { useForm } from "react-hook-form";
+import { getAccountCredentials } from "~/libs/getAccountCredentials";
+import { supabase } from "~/libs/supabase";
+
+type FormValues = {
+ id: string;
+};
+
+export default function Signin(): ReactNode {
+ // ID でサインイン、デバッグ用(そもそもログアウトは想定していない)
+ const navigate = useNavigate();
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ mode: "onBlur",
+ });
+
+ const onSubmit = async ({ id }: FormValues) => {
+ const { error } = await supabase.auth.signInWithPassword(
+ getAccountCredentials(id),
+ );
+
+ if (error) {
+ console.error(error);
+ return;
+ }
+
+ navigate("/");
+ };
+
+ return (
+
+ Game
+ ID でサインイン
+
+
+
+ );
+}
diff --git a/app/routes/_noauth.signup.tsx b/app/routes/_noauth.signup.tsx
new file mode 100644
index 0000000..d43eab7
--- /dev/null
+++ b/app/routes/_noauth.signup.tsx
@@ -0,0 +1,34 @@
+import { useNavigate } from "@remix-run/react";
+import { v4 } from "uuid";
+import { getAccountCredentials } from "~/libs/getAccountCredentials";
+import { supabase } from "~/libs/supabase";
+
+export default function Signup() {
+ const navigate = useNavigate();
+
+ const signUp = async () => {
+ const id = v4();
+ const { error } = await supabase.auth.signUp(getAccountCredentials(id));
+
+ if (error) {
+ console.error("Error signing up:", error.message);
+ return;
+ }
+
+ navigate("/");
+ };
+ return (
+
+ Game
+
+
+
+
+ );
+}
diff --git a/app/routes/_noauth.tsx b/app/routes/_noauth.tsx
new file mode 100644
index 0000000..24dd24a
--- /dev/null
+++ b/app/routes/_noauth.tsx
@@ -0,0 +1,19 @@
+import { Outlet, useNavigate } from "@remix-run/react";
+import type { ReactNode } from "react";
+import { useLayoutEffect } from "react";
+
+import { useSession } from "~/hooks/useSession";
+
+// _noauth.**.tsx のパスへのアクセスは必ずここで前処理される
+export default function Layout(): ReactNode {
+ const navigate = useNavigate();
+ const session = useSession();
+
+ useLayoutEffect(() => {
+ if (session) {
+ navigate("/");
+ }
+ }, [session, navigate]);
+
+ return ;
+}
diff --git a/biome.jsonc b/biome.jsonc
index 8f255b3..b7f6fa3 100644
--- a/biome.jsonc
+++ b/biome.jsonc
@@ -6,7 +6,10 @@
"linter": {
"enabled": true,
"rules": {
- "recommended": true
+ "recommended": true,
+ "nursery": {
+ "useSortedClasses": "warn"
+ }
}
},
"formatter": {
diff --git a/package-lock.json b/package-lock.json
index d47a815..6403d9a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,11 +10,14 @@
"@remix-run/react": "^2.12.0",
"@remix-run/serve": "^2.12.0",
"@supabase/supabase-js": "^2.45.3",
+ "@types/uuid": "^10.0.0",
"isbot": "^4.1.0",
"qrcode.react": "^4.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "swr": "^2.2.5"
+ "react-hook-form": "^7.53.0",
+ "swr": "^2.2.5",
+ "uuid": "^10.0.0"
},
"devDependencies": {
"@biomejs/biome": "^1.8.3",
@@ -2867,6 +2870,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "license": "MIT"
+ },
"node_modules/@types/ws": {
"version": "8.5.12",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
@@ -10533,6 +10542,22 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.53.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz",
+ "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -12533,6 +12558,19 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/uvu": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
diff --git a/package.json b/package.json
index 88a677d..4b3c4ad 100644
--- a/package.json
+++ b/package.json
@@ -9,18 +9,21 @@
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc",
"lint": "biome check .",
- "fmt": "biome check . --write"
+ "fmt": "biome check . --write --unsafe"
},
"dependencies": {
"@remix-run/node": "^2.12.0",
"@remix-run/react": "^2.12.0",
"@remix-run/serve": "^2.12.0",
"@supabase/supabase-js": "^2.45.3",
+ "@types/uuid": "^10.0.0",
"isbot": "^4.1.0",
"qrcode.react": "^4.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "swr": "^2.2.5"
+ "react-hook-form": "^7.53.0",
+ "swr": "^2.2.5",
+ "uuid": "^10.0.0"
},
"devDependencies": {
"@biomejs/biome": "^1.8.3",
diff --git a/vite.config.ts b/vite.config.ts
index e07fb91..2713971 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -10,6 +10,7 @@ export default defineConfig({
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
},
+ ssr: false,
}),
tsconfigPaths(),
],