From 05647fa1714c9d5372dfa9304f3e816b58952102 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Mon, 19 Aug 2024 21:20:35 +0900 Subject: [PATCH 01/28] Add: Show /index --- .gitignore | 144 + biome.jsonc | 28 + frontend/.dockerignore | 1 - frontend/.eslintrc.js | 29 - frontend/.gitignore | 38 - frontend/.node-version | 1 - frontend/.prettierrc.json | 7 - frontend/@types/data.d.ts | 54 - frontend/@types/yml.d.ts | 5 - frontend/Dockerfile | 16 - frontend/{pages => app}/admin.tsx | 115 +- frontend/app/entry.client.tsx | 39 + frontend/app/entry.server.tsx | 75 + frontend/{pages => app}/login.tsx | 2 +- frontend/app/root.tsx | 90 + frontend/app/routes/$.tsx | 10 + frontend/app/routes/_index.tsx | 94 + frontend/assets/404.svg | 210 + frontend/{public => assets}/favicon.svg | 0 frontend/{public => assets}/logo-cf.svg | 0 .../client/assets/components-De1Nnuln.js | 207 + .../client/assets/entry.client-lorunqDK.js | 19 + .../build/client/assets/manifest-dc0424cc.js | 1 + frontend/build/client/assets/root-B6IWK6oX.js | 1 + frontend/build/server/index.js | 164 + frontend/components/ChartCard.tsx | 50 +- frontend/components/Checkbox.tsx | 10 +- frontend/components/Footer.tsx | 14 +- frontend/components/Header.tsx | 78 +- frontend/components/ModalPortal.tsx | 2 +- frontend/components/OptionalImage.tsx | 19 +- frontend/components/SideMenu.tsx | 67 +- frontend/components/TextInput.tsx | 138 +- frontend/i18n.js | 37 - frontend/i18n/en.yml | 5 + frontend/i18n/ja.yml | 5 + frontend/lib/atom.ts | 9 - frontend/lib/contexts.ts | 8 + frontend/lib/i18n.ts | 14 + frontend/lib/index.ts | 2 - frontend/lib/requireLogin.tsx | 20 - frontend/lib/translations.ts | 19 + frontend/lib/types.ts | 53 + frontend/lib/useLogin.ts | 49 - frontend/lib/utils.ts | 23 +- frontend/middleware.ts | 24 - frontend/next-env.d.ts | 5 - frontend/next.config.js | 85 - frontend/package.json | 94 +- frontend/pages/charts/[name].tsx | 173 +- frontend/pages/index.tsx | 117 - .../@fluentui__react-icons@2.0.253.patch | 1077 +++ frontend/pnpm-lock.yaml | 8084 ++++++++--------- .../{postcss.config.js => postcss.config.cjs} | 2 +- frontend/remix-env.d.ts | 5 + frontend/sentry.client.config.ts | 14 - frontend/sentry.edge.config.ts | 3 - frontend/sentry.server.config.ts | 12 - frontend/styles/globals.scss | 19 +- frontend/tailwind.config.ts | 8 +- frontend/tsconfig.json | 43 +- frontend/vite.config.ts | 33 + 62 files changed, 6289 insertions(+), 5481 deletions(-) create mode 100644 biome.jsonc delete mode 100644 frontend/.dockerignore delete mode 100644 frontend/.eslintrc.js delete mode 100644 frontend/.gitignore delete mode 100644 frontend/.node-version delete mode 100644 frontend/.prettierrc.json delete mode 100644 frontend/@types/data.d.ts delete mode 100644 frontend/@types/yml.d.ts delete mode 100644 frontend/Dockerfile rename frontend/{pages => app}/admin.tsx (69%) create mode 100644 frontend/app/entry.client.tsx create mode 100644 frontend/app/entry.server.tsx rename frontend/{pages => app}/login.tsx (97%) create mode 100644 frontend/app/root.tsx create mode 100644 frontend/app/routes/$.tsx create mode 100644 frontend/app/routes/_index.tsx create mode 100644 frontend/assets/404.svg rename frontend/{public => assets}/favicon.svg (100%) rename frontend/{public => assets}/logo-cf.svg (100%) create mode 100644 frontend/build/client/assets/components-De1Nnuln.js create mode 100644 frontend/build/client/assets/entry.client-lorunqDK.js create mode 100644 frontend/build/client/assets/manifest-dc0424cc.js create mode 100644 frontend/build/client/assets/root-B6IWK6oX.js create mode 100644 frontend/build/server/index.js delete mode 100644 frontend/i18n.js delete mode 100644 frontend/lib/atom.ts create mode 100644 frontend/lib/contexts.ts create mode 100644 frontend/lib/i18n.ts delete mode 100644 frontend/lib/index.ts delete mode 100644 frontend/lib/requireLogin.tsx create mode 100644 frontend/lib/translations.ts create mode 100644 frontend/lib/types.ts delete mode 100644 frontend/lib/useLogin.ts delete mode 100644 frontend/middleware.ts delete mode 100644 frontend/next-env.d.ts delete mode 100644 frontend/next.config.js delete mode 100644 frontend/pages/index.tsx create mode 100644 frontend/patches/@fluentui__react-icons@2.0.253.patch rename frontend/{postcss.config.js => postcss.config.cjs} (96%) create mode 100644 frontend/remix-env.d.ts delete mode 100644 frontend/sentry.client.config.ts delete mode 100644 frontend/sentry.edge.config.ts delete mode 100644 frontend/sentry.server.config.ts create mode 100644 frontend/vite.config.ts diff --git a/.gitignore b/.gitignore index 92a2788..2023401 100644 --- a/.gitignore +++ b/.gitignore @@ -156,3 +156,147 @@ test_results/* sub-image/tmp config.yml +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..b03496f --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,28 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "organizeImports": { + "enabled": true + }, + "formatter": { + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "nursery": { + "useImportExtensions": "error" + }, + "a11y": { + "all": false + }, + "suspicious": { + "noArrayIndexKey": "off" + }, + "style": { + "noNonNullAssertion": "off", + "noUselessElse": "off" + } + } + } +} diff --git a/frontend/.dockerignore b/frontend/.dockerignore deleted file mode 100644 index 3c3629e..0000000 --- a/frontend/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js deleted file mode 100644 index 06e8c7e..0000000 --- a/frontend/.eslintrc.js +++ /dev/null @@ -1,29 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint", "prettier", "import"], - extends: [ - "next/core-web-vitals", - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "prettier", - ], - ignorePatterns: ["*.config.js", "dist"], - rules: { - "prettier/prettier": "error", - "import/order": "error", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - destructuredArrayIgnorePattern: "^_", - }, - ], - "no-control-regex": "off", - "react/prop-types": "off", - }, -} diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 23d2222..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,38 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo - -*.local -*.local.* diff --git a/frontend/.node-version b/frontend/.node-version deleted file mode 100644 index d939939..0000000 --- a/frontend/.node-version +++ /dev/null @@ -1 +0,0 @@ -18.13.0 diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json deleted file mode 100644 index 9ef053a..0000000 --- a/frontend/.prettierrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "tabWidth": 2, - "singleQuote": false, - "semi": false, - "trailingComma": "es5", - "endOfLine": "lf" -} diff --git a/frontend/@types/data.d.ts b/frontend/@types/data.d.ts deleted file mode 100644 index ef64f34..0000000 --- a/frontend/@types/data.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -interface Chart { - name: string - title: string - composer: string - artist: string | undefined - author: User - authorName: string - coAuthors: User[] - publishedAt: string - updatedAt: string - cover: string - bgm: string - chart: { - url: string | undefined - type: "sus" | "mmws" | "chs" - } - data: string | undefined - variants: Chart[] - variantOf: Chart | undefined - tags: string[] - visibility: "public" | "private" | "scheduled" - isChartPublic: boolean - scheduledAt: string | undefined - rating: number - description: string - likes: number -} - -interface User { - handle: string - name: string - aboutMe: string - bgColor: string - fgColor: string - chartCount: number -} - -type AdminUser = User & { altUsers: User[] } - -type DiscordInfo = { - displayName: string - username: string - avatar: string -} - -type Session = - | { - loggedIn: true - user: User - altUsers: User[] - discord: DiscordInfo | undefined - } - | { loggedIn: false } - | { loggedIn: undefined } diff --git a/frontend/@types/yml.d.ts b/frontend/@types/yml.d.ts deleted file mode 100644 index 9b159f1..0000000 --- a/frontend/@types/yml.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "*.yml" { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const value: any - export default value -} diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 8cf78b4..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM node:18-slim - -WORKDIR /app - -RUN apt-get update && apt-get install -y curl - -COPY frontend/package.json frontend/pnpm-lock.yaml ./ - -ENV NODE_ENV=production -ENV CI=true -RUN npm install -g pnpm && pnpm install --frozen-lockfile - -COPY frontend/. . - -EXPOSE 3100 -CMD ["pnpm", "start"] diff --git a/frontend/pages/admin.tsx b/frontend/app/admin.tsx similarity index 69% rename from frontend/pages/admin.tsx rename to frontend/app/admin.tsx index 7b79b7b..73ce246 100644 --- a/frontend/pages/admin.tsx +++ b/frontend/app/admin.tsx @@ -1,69 +1,80 @@ -import { NextPage } from "next" -import Head from "next/head" -import useTranslation from "next-translate/useTranslation" -import Link from "next/link" -import { useRouter } from "next/router" -import { useCallback, useEffect, useState } from "react" -import requireLogin from "lib/requireLogin" -import { className } from "lib/utils" +import { useCallback, useEffect, useState } from "react"; +import { Link, type MetaFunction, json, useNavigate } from "@remix-run/react"; +import requireLogin from "lib/requireLogin"; +import {useTranslation} from "react-i18next"; +import clsx from "clsx"; +import {getFixedT} from "i18next"; +import {LoaderFunctionArgs} from "@remix-run/node"; -const Admin: NextPage = () => { - const { t } = useTranslation("admin") - const { t: rootT } = useTranslation() - const router = useRouter() +export async function loader({ request }: LoaderFunctionArgs) { + const rootT = getFixedT(request); + const t = getFixedT(request) + const title = `${t("title")} | ${rootT("name")}` + return json({ title }); +} + +// meta +export const meta: MetaFunction< + typeof loader +> = ({ data }) => { + // metaは翻訳されたものをセットするだけ + return { title: data.title }; +}; + +const Admin = () => { + const { t } = useTranslation("admin"); + const { t: rootT } = useTranslation(); + const navigate = useNavigate(); const fetchAdmin = useCallback(() => { fetch("/api/admin").then(async (res) => { - const json = await res.json() + const json = await res.json(); if (json.code === "forbidden") { - router.push("/") + navigate("/"); } - setData(json.data) - }) - }, [router]) + setData(json.data); + }); + }, [navigate]); useEffect(() => { - fetchAdmin() - const interval = setInterval(fetchAdmin, 10000) - return () => clearInterval(interval) - }, [fetchAdmin]) + fetchAdmin(); + const interval = setInterval(fetchAdmin, 10000); + return () => clearInterval(interval); + }, [fetchAdmin]); const [data, setData] = useState<{ stats: { charts: { - public: number - private: number - } + public: number; + private: number; + }; users: { - original: number - alt: number - discord: number - } - files: Record + original: number; + alt: number; + discord: number; + }; + files: Record; db: { - size: number - connections: number - busy: number - dead: number - idle: number - waiting: number - checkout_timeout: number - } - } - } | null>(null) + size: number; + connections: number; + busy: number; + dead: number; + idle: number; + waiting: number; + checkout_timeout: number; + }; + }; + } | null>(null); - const card = "bg-slate-100 dark:bg-slate-800 rounded-md p-4" - const statCard = className(card, "w-full md:w-80") - const actionCard = className(card, "w-full") + const card = "bg-slate-100 dark:bg-slate-800 rounded-md p-4"; + const statCard = clsx(card, "w-full md:w-80"); + const actionCard = clsx(card, "w-full"); - if (!data) return null + if (!data) return null; return ( <> - - {t("title") + " | " + rootT("name")} -

{t("title")}

@@ -128,7 +139,7 @@ const Admin: NextPage = () => {

{t("sidekiq.title")}

- + {t("sidekiq.description")}

@@ -148,8 +159,8 @@ const Admin: NextPage = () => { data: { count }, } = await fetch("/api/admin/expire-data", { method: "POST", - }).then((res) => res.json()) - alert(t("actions.expireData.success", { count })) + }).then((res) => res.json()); + alert(t("actions.expireData.success", { count })); }} > {t("actions.expireData.button")} @@ -158,7 +169,7 @@ const Admin: NextPage = () => {
- ) -} + ); +}; -export default requireLogin(Admin) +export default requireLogin(Admin); diff --git a/frontend/app/entry.client.tsx b/frontend/app/entry.client.tsx new file mode 100644 index 0000000..8ec56f3 --- /dev/null +++ b/frontend/app/entry.client.tsx @@ -0,0 +1,39 @@ +import { RemixBrowser } from "@remix-run/react"; +import i18next from "i18next"; +import { StrictMode, startTransition } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { I18nextProvider, initReactI18next } from "react-i18next"; +import { enTranslation, jaTranslation } from "~/lib/translations"; +import languageDetector from "i18next-browser-languagedetector"; + +i18next + .use(initReactI18next) // passes i18n down to react-i18next + .use(languageDetector) + .init({ + resources: { + ja: jaTranslation, + en: enTranslation, + }, + fallbackLng: "en", + defaultNS: "root", + + interpolation: { + escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape + }, + + detection: { + order: ["htmlTag"], + caches: [], + }, + }); + +startTransition(() => { + hydrateRoot( + document, + + + + + , + ); +}); diff --git a/frontend/app/entry.server.tsx b/frontend/app/entry.server.tsx new file mode 100644 index 0000000..f8c48e8 --- /dev/null +++ b/frontend/app/entry.server.tsx @@ -0,0 +1,75 @@ +import { + type EntryContext, + createReadableStreamFromReadable, +} from "@remix-run/node"; +import { createInstance } from "i18next"; +import { renderToPipeableStream } from "react-dom/server"; +import { PassThrough } from "node:stream"; +import { I18nextProvider, initReactI18next } from "react-i18next"; +import { i18n } from "~/lib/i18n"; +import { enTranslation, jaTranslation } from "~/lib/translations"; +import { RemixServer } from "@remix-run/react"; +import { isbot } from "isbot"; + +const ABORT_DELAY = 5000; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + const lng = await i18n.getLocale(request); + const ns = i18n.getRouteNamespaces(remixContext); + const callbackName = isbot(request.headers.get("user-agent")) + ? "onAllReady" + : "onShellReady"; + const instance = createInstance(); + + await instance.use(initReactI18next).init({ + lng, // The locale we detected above + ns, // The namespaces the routes about to render want to use + resources: { + ja: jaTranslation, + en: enTranslation, + }, + fallbackLng: "en", + defaultNS: "root", + }); + + return new Promise((resolve, reject) => { + let didError = false; + + const { pipe, abort } = renderToPipeableStream( + + + , + { + [callbackName]: () => { + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: didError ? 500 : responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + didError = true; + + console.error(error); + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/frontend/pages/login.tsx b/frontend/app/login.tsx similarity index 97% rename from frontend/pages/login.tsx rename to frontend/app/login.tsx index bf7d063..f699f8a 100644 --- a/frontend/pages/login.tsx +++ b/frontend/app/login.tsx @@ -25,7 +25,7 @@ const Login: NextPage = () => { loginStarted.current = true startLogin() } - }, [loginState, startLogin]) + }, [startLogin]) return (
diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx new file mode 100644 index 0000000..fc3a7ed --- /dev/null +++ b/frontend/app/root.tsx @@ -0,0 +1,90 @@ +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; +import type { LinksFunction } from "@remix-run/node"; +import i18n from "i18next"; +import { initReactI18next, useTranslation } from "react-i18next"; +import favicon from "~/assets/favicon.svg?url"; +import Footer from "~/components/Footer.tsx"; +import Header from "~/components/Header.tsx"; +import enTranslation from "~/i18n/en.yml"; +import jaTranslation from "~/i18n/ja.yml"; +import { SessionContext } from "~/lib/contexts"; +import styles from "~/styles/globals.scss?url"; +import { useEffect, useState } from "react"; +import type { Session } from "~/lib/types"; + +export const links: LinksFunction = () => { + return [ + { rel: "stylesheet", href: styles }, + { + rel: "icon", + type: "image/svg+xml", + href: favicon, + }, + ]; +}; + +export default function App() { + return ; +} + +export function Layout({ children }: { children: React.ReactNode }) { + const [session, setSession] = useState(undefined); + const { i18n } = useTranslation(); + useEffect(() => { + if (session && session.loggedIn !== undefined) { + return; + } + fetch("/api/login/session", { + method: "GET", + }).then(async (res) => { + const json = await res.json(); + if (json.code === "ok") { + const [altUsers, discordUser] = await Promise.all([ + fetch("/api/my/alt_users").then( + async (res) => (await res.json()).users, + ), + fetch("/api/my/discord").then( + async (res) => (await res.json()).discord, + ), + ]); + + setSession({ + loggedIn: true, + user: json.user, + altUsers, + discord: discordUser, + }); + } else { + setSession({ loggedIn: false }); + } + }); + }, [session]); + return ( + + + + + + + + +
+
+ {children} +
+ +