diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..67f37e7
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,21 @@
+# Dependencies and build outputs (reinstalled / rebuilt in image)
+node_modules
+**/node_modules
+.next
+**/.next
+
+# Git
+.git
+.gitignore
+
+# Other projects in the repo (not needed for the Next.js image)
+main-web
+tools
+twitch_chat_reader
+infra
+
+# Misc
+*.md
+.env
+.env.*
+!.env.example
diff --git a/build.sh b/build.sh
index 1c85fe4..75e9d97 100644
--- a/build.sh
+++ b/build.sh
@@ -1,63 +1,2 @@
-#!/bin/sh
-
-# create folder to pack all
-echo "Starting build process..."
-mkdir build
-
-# build janken tool and move
-cd tools/janken
-echo "Starting janken tool build..."
-npm run build
-cd ../../
-mv tools/janken/tool build
-echo "Finished janken tool build..."
-
-# build client and move
-cd main-web/client
-echo "Starting client build..."
-npm run build
-cd ../../
-mv main-web/client/client build
-echo "Finished client build..."
-
-# build admin and move
-cd main-web/admin
-echo "Starting admin ui build..."
-npm run build
-cd ../../
-mv main-web/admin/build build/admin
-echo "Finished admin ui build..."
-
-# build server and move
-cd main-web/server
-echo "Starting server build..."
-npm run build
-cd ../../
-cd build/
-echo "Starting retrieval of db credentials..."
-export catherine_db_endpoint=$(aws --region=ap-northeast-1 ssm get-parameter --name "db-endpoint" --with-decryption --output text --query Parameter.Value)
-export catherine_db_user=$(aws --region=ap-northeast-1 ssm get-parameter --name "db-username" --with-decryption --output text --query Parameter.Value)
-export catherine_db_pass=$(aws --region=ap-northeast-1 ssm get-parameter --name "db-password" --with-decryption --output text --query Parameter.Value)
-echo "Saving db credentials to .env file..."
-cat > .env <
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/nextjs/next.config.mjs b/nextjs/next.config.mjs
new file mode 100644
index 0000000..9e53c8e
--- /dev/null
+++ b/nextjs/next.config.mjs
@@ -0,0 +1,15 @@
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ output: 'standalone',
+ outputFileTracingRoot: path.join(__dirname, '..'),
+ sassOptions: {
+ includePaths: ['src/styles'],
+ },
+};
+
+export default nextConfig;
diff --git a/nextjs/package.json b/nextjs/package.json
new file mode 100644
index 0000000..5596895
--- /dev/null
+++ b/nextjs/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "catherine-league-nextjs",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "dotenv": "^16.4.5",
+ "i18next": "^23.10.1",
+ "mysql2": "^3.11.0",
+ "next": "^15.5.14",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-i18next": "^14.1.0",
+ "react-youtube": "^10.1.0",
+ "semantic-ui-css": "^2.5.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.14.10",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "eslint": "^8.57.0",
+ "eslint-config-next": "^15.5.14",
+ "sass": "^1.77.8",
+ "typescript": "^5.5.4"
+ }
+}
diff --git a/nextjs/public/manifest.json b/nextjs/public/manifest.json
new file mode 100644
index 0000000..366d447
--- /dev/null
+++ b/nextjs/public/manifest.json
@@ -0,0 +1,9 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#ff2d7c",
+ "background_color": "#ffffff"
+}
diff --git a/nextjs/public/robots.txt b/nextjs/public/robots.txt
new file mode 100644
index 0000000..e9e57dc
--- /dev/null
+++ b/nextjs/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/nextjs/src/app/about/page.tsx b/nextjs/src/app/about/page.tsx
new file mode 100644
index 0000000..b9bc992
--- /dev/null
+++ b/nextjs/src/app/about/page.tsx
@@ -0,0 +1,34 @@
+import style from '@/styles/web.module.scss';
+
+export default function AboutPage() {
+ return (
+
+
About
+
+
+
+
+ Covid-19
+ の影響で、オフライン大会の実施なども困難であるため、オンラインによるランキングバトル(通称:ストレイシープ杯)を開催することになりました。
+
+
+ 先日発売されたSwitch版+各自の配信設備を用いて大会を実施するため、まずは少人数で進めていきます。(まずは第0回を実験的に開催)
+
+
+ キャサリンの対戦は、決着はわかりやすいですが、使われているテクニックなどが分かるともっと楽しめると思うので、解説なども交えながら実施する予定です。
+
+
悪い点は改善し、良い点はつづけながら、継続的に続けていきたいと思います。
+
+ もっとこうしてみては?次の大会から参加したい!などございましたら問い合わせフォームからご意見下さい!
+
+
それでは、楽しんでいくめぇ。~
+
+
+
+
+ );
+}
diff --git a/nextjs/src/app/api/archive/route.ts b/nextjs/src/app/api/archive/route.ts
new file mode 100644
index 0000000..8fa178b
--- /dev/null
+++ b/nextjs/src/app/api/archive/route.ts
@@ -0,0 +1,15 @@
+import { NextResponse } from 'next/server';
+
+import { getArchiveItems } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ try {
+ const data = await getArchiveItems();
+ return NextResponse.json(data);
+ } catch (e) {
+ console.error(e);
+ return NextResponse.json([], { status: 500 });
+ }
+}
diff --git a/nextjs/src/app/api/contact/route.ts b/nextjs/src/app/api/contact/route.ts
new file mode 100644
index 0000000..4c42090
--- /dev/null
+++ b/nextjs/src/app/api/contact/route.ts
@@ -0,0 +1,15 @@
+import { NextResponse } from 'next/server';
+
+import { getContactItems } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ try {
+ const data = await getContactItems();
+ return NextResponse.json(data);
+ } catch (e) {
+ console.error(e);
+ return NextResponse.json([], { status: 500 });
+ }
+}
diff --git a/nextjs/src/app/api/guide/route.ts b/nextjs/src/app/api/guide/route.ts
new file mode 100644
index 0000000..1f6dfd8
--- /dev/null
+++ b/nextjs/src/app/api/guide/route.ts
@@ -0,0 +1,15 @@
+import { NextResponse } from 'next/server';
+
+import { getGuideItems } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ try {
+ const data = await getGuideItems();
+ return NextResponse.json(data);
+ } catch (e) {
+ console.error(e);
+ return NextResponse.json([], { status: 500 });
+ }
+}
diff --git a/nextjs/src/app/api/players/[key]/route.ts b/nextjs/src/app/api/players/[key]/route.ts
new file mode 100644
index 0000000..216e9e0
--- /dev/null
+++ b/nextjs/src/app/api/players/[key]/route.ts
@@ -0,0 +1,23 @@
+import { NextResponse } from 'next/server';
+
+import { getPlayersForTournament } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+type RouteContext = {
+ params: Promise<{ key: string }>;
+};
+
+export async function GET(_request: Request, context: RouteContext) {
+ try {
+ const { key } = await context.params;
+ const data = await getPlayersForTournament(key);
+ if (data === null) {
+ return NextResponse.json({ error: 'Tournament not found' }, { status: 404 });
+ }
+ return NextResponse.json(data);
+ } catch (e) {
+ console.error(e);
+ return NextResponse.json([], { status: 500 });
+ }
+}
diff --git a/nextjs/src/app/api/players/route.ts b/nextjs/src/app/api/players/route.ts
new file mode 100644
index 0000000..f19c198
--- /dev/null
+++ b/nextjs/src/app/api/players/route.ts
@@ -0,0 +1,15 @@
+import { NextResponse } from 'next/server';
+
+import { getAllPlayers } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ try {
+ const data = await getAllPlayers();
+ return NextResponse.json(data);
+ } catch (e) {
+ console.error(e);
+ return NextResponse.json([], { status: 500 });
+ }
+}
diff --git a/nextjs/src/app/archive/page.tsx b/nextjs/src/app/archive/page.tsx
new file mode 100644
index 0000000..fe70e62
--- /dev/null
+++ b/nextjs/src/app/archive/page.tsx
@@ -0,0 +1,9 @@
+import { ArchiveVideoList } from '@/components/ArchiveVideoList';
+import { getArchiveItems } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+export default async function ArchivePage() {
+ const items = await getArchiveItems();
+ return ;
+}
diff --git a/nextjs/src/app/contact/page.tsx b/nextjs/src/app/contact/page.tsx
new file mode 100644
index 0000000..1c7a39c
--- /dev/null
+++ b/nextjs/src/app/contact/page.tsx
@@ -0,0 +1,9 @@
+import { ContactList } from '@/components/ContactList';
+import { getContactItems } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+export default async function ContactPage() {
+ const items = await getContactItems();
+ return ;
+}
diff --git a/nextjs/src/app/globals.css b/nextjs/src/app/globals.css
new file mode 100644
index 0000000..c6ef281
--- /dev/null
+++ b/nextjs/src/app/globals.css
@@ -0,0 +1,17 @@
+body {
+ margin: 0;
+ font-family: var(--font-mplus), -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
+
+html,
+body {
+ min-height: 100%;
+}
diff --git a/nextjs/src/app/guide/page.tsx b/nextjs/src/app/guide/page.tsx
new file mode 100644
index 0000000..afe2795
--- /dev/null
+++ b/nextjs/src/app/guide/page.tsx
@@ -0,0 +1,9 @@
+import { GuideVideoList } from '@/components/GuideVideoList';
+import { getGuideItems } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+export default async function GuidePage() {
+ const items = await getGuideItems();
+ return ;
+}
diff --git a/nextjs/src/app/layout.tsx b/nextjs/src/app/layout.tsx
new file mode 100644
index 0000000..370c6f1
--- /dev/null
+++ b/nextjs/src/app/layout.tsx
@@ -0,0 +1,46 @@
+import type { Metadata, Viewport } from 'next';
+import { Indie_Flower, M_PLUS_Rounded_1c } from 'next/font/google';
+import type { ReactNode } from 'react';
+
+import { I18nProvider } from '@/components/I18nProvider';
+import { SiteLayout } from '@/components/SiteLayout';
+
+import 'semantic-ui-css/semantic.min.css';
+import './globals.css';
+
+const indieFlower = Indie_Flower({
+ weight: '400',
+ subsets: ['latin'],
+ variable: '--font-indie',
+ display: 'swap',
+});
+
+const mPlusRounded = M_PLUS_Rounded_1c({
+ weight: ['400', '700'],
+ subsets: ['latin'],
+ variable: '--font-mplus',
+ display: 'swap',
+ adjustFontFallback: false,
+});
+
+export const metadata: Metadata = {
+ title: 'キャサリン faito crab',
+ description: 'キャサリン faito crab',
+ manifest: '/manifest.json',
+};
+
+export const viewport: Viewport = {
+ themeColor: '#ff2d7c',
+};
+
+export default function RootLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/nextjs/src/app/not-found.tsx b/nextjs/src/app/not-found.tsx
new file mode 100644
index 0000000..581b890
--- /dev/null
+++ b/nextjs/src/app/not-found.tsx
@@ -0,0 +1,7 @@
+export default function NotFound() {
+ return (
+
+ );
+}
diff --git a/nextjs/src/app/page.tsx b/nextjs/src/app/page.tsx
new file mode 100644
index 0000000..2e3d61d
--- /dev/null
+++ b/nextjs/src/app/page.tsx
@@ -0,0 +1,34 @@
+import style from '@/styles/web.module.scss';
+
+export default function HomePage() {
+ return (
+
+ );
+}
diff --git a/nextjs/src/app/players/page.tsx b/nextjs/src/app/players/page.tsx
new file mode 100644
index 0000000..724ae4f
--- /dev/null
+++ b/nextjs/src/app/players/page.tsx
@@ -0,0 +1,9 @@
+import { PlayersList } from '@/components/PlayersList';
+import { getAllPlayers } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+export default async function PlayersPage() {
+ const players = await getAllPlayers();
+ return ;
+}
diff --git a/nextjs/src/app/tournaments/[tournament_key]/players/page.tsx b/nextjs/src/app/tournaments/[tournament_key]/players/page.tsx
new file mode 100644
index 0000000..07cd7ba
--- /dev/null
+++ b/nextjs/src/app/tournaments/[tournament_key]/players/page.tsx
@@ -0,0 +1,19 @@
+import { notFound } from 'next/navigation';
+
+import { PlayersList } from '@/components/PlayersList';
+import { getPlayersForTournament } from '@/lib/data';
+
+export const dynamic = 'force-dynamic';
+
+type PageProps = {
+ params: Promise<{ tournament_key: string }>;
+};
+
+export default async function TournamentPlayersPage({ params }: PageProps) {
+ const { tournament_key } = await params;
+ const players = await getPlayersForTournament(tournament_key);
+ if (players === null) {
+ notFound();
+ }
+ return ;
+}
diff --git a/nextjs/src/app/tournaments/[tournament_key]/scoreboard/page.tsx b/nextjs/src/app/tournaments/[tournament_key]/scoreboard/page.tsx
new file mode 100644
index 0000000..7264a23
--- /dev/null
+++ b/nextjs/src/app/tournaments/[tournament_key]/scoreboard/page.tsx
@@ -0,0 +1,9 @@
+import style from '@/styles/web.module.scss';
+
+export default function ScoreboardPage() {
+ return (
+
+ );
+}
diff --git a/nextjs/src/app/tournaments/page.tsx b/nextjs/src/app/tournaments/page.tsx
new file mode 100644
index 0000000..23ac0d1
--- /dev/null
+++ b/nextjs/src/app/tournaments/page.tsx
@@ -0,0 +1,7 @@
+export default function TournamentsPage() {
+ return (
+
+ );
+}
diff --git a/nextjs/src/components/ArchiveVideoList.tsx b/nextjs/src/components/ArchiveVideoList.tsx
new file mode 100644
index 0000000..fde444f
--- /dev/null
+++ b/nextjs/src/components/ArchiveVideoList.tsx
@@ -0,0 +1,43 @@
+'use client';
+
+import { useTranslation } from 'react-i18next';
+import YouTube from 'react-youtube';
+
+import type { IGuideItem } from '@/models/IGuideItem';
+import style from '@/styles/web.module.scss';
+
+type Props = {
+ items: IGuideItem[];
+};
+
+const opts = {
+ height: '195',
+ width: '320',
+};
+
+export function ArchiveVideoList({ items }: Props) {
+ const { t } = useTranslation();
+
+ return (
+
+
{t('archive')}
+
+
+ {items.map((el) => (
+
+ ))}
+
+ );
+}
diff --git a/nextjs/src/components/ContactList.tsx b/nextjs/src/components/ContactList.tsx
new file mode 100644
index 0000000..f8496ec
--- /dev/null
+++ b/nextjs/src/components/ContactList.tsx
@@ -0,0 +1,39 @@
+'use client';
+
+import { useTranslation } from 'react-i18next';
+
+import type { IContactQuestion } from '@/models/IContactQuestion';
+import style from '@/styles/web.module.scss';
+
+type Props = {
+ items: IContactQuestion[];
+};
+
+export function ContactList({ items }: Props) {
+ const { t } = useTranslation();
+
+ return (
+
+
{t('qanda')}
+
+
+ {items.map((el) => (
+
+
{el.question}
+
+
{el.answer}
+
+ ))}
+
+ );
+}
diff --git a/nextjs/src/components/GuideVideoList.tsx b/nextjs/src/components/GuideVideoList.tsx
new file mode 100644
index 0000000..fe8a708
--- /dev/null
+++ b/nextjs/src/components/GuideVideoList.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import { useTranslation } from 'react-i18next';
+import YouTube from 'react-youtube';
+
+import type { IGuideItem } from '@/models/IGuideItem';
+import style from '@/styles/web.module.scss';
+
+type Props = {
+ items: IGuideItem[];
+};
+
+const opts = {
+ height: '195',
+ width: '320',
+};
+
+export function GuideVideoList({ items }: Props) {
+ const { t } = useTranslation();
+
+ return (
+
+
{t('guide')}
+
+
+ {items.map((el) => (
+
+
{el.title}
+
+
{el.description}
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/nextjs/src/components/I18nProvider.tsx b/nextjs/src/components/I18nProvider.tsx
new file mode 100644
index 0000000..37cb5f3
--- /dev/null
+++ b/nextjs/src/components/I18nProvider.tsx
@@ -0,0 +1,10 @@
+'use client';
+
+import { I18nextProvider } from 'react-i18next';
+import type { ReactNode } from 'react';
+
+import i18n from '@/i18n/client';
+
+export function I18nProvider({ children }: { children: ReactNode }) {
+ return {children};
+}
diff --git a/nextjs/src/components/PlayersList.tsx b/nextjs/src/components/PlayersList.tsx
new file mode 100644
index 0000000..cf71cbb
--- /dev/null
+++ b/nextjs/src/components/PlayersList.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import { useTranslation } from 'react-i18next';
+
+import type { IPlayerInfo } from '@/models/IPlayerInfo';
+import style from '@/styles/web.module.scss';
+
+type Props = {
+ players: IPlayerInfo[];
+};
+
+export function PlayersList({ players }: Props) {
+ const { t } = useTranslation();
+
+ return (
+
+
Players
+
+
+ {players.map((el) => {
+ const charClass =
+ style[`character-${el.name_id}` as keyof typeof style] ?? '';
+ return (
+
+
+
+ {t('player_name')}
+
+
{el.player_name}
+
+ {t('use_character')}
+
+
{el.name_jp}
+
+ {t('description')}
+
+
{el.description}
+
+ );
+ })}
+
+ );
+}
diff --git a/nextjs/src/components/SiteLayout.tsx b/nextjs/src/components/SiteLayout.tsx
new file mode 100644
index 0000000..45163bf
--- /dev/null
+++ b/nextjs/src/components/SiteLayout.tsx
@@ -0,0 +1,114 @@
+import Link from 'next/link';
+import type { ReactNode } from 'react';
+
+import style from '@/styles/web.module.scss';
+
+export function SiteLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+
{children}
+
+
+
+
+ ©ATLUS
+ {' '}
+ ©SEGA All Rights Reserved.
+
+
本ページは、株式会社ATLUS様のページとは異なるユーザーサイトです。
+
問い合わせなどをATLUS様に出されないよう、ご注意ください。
+
+
+
+
+
+
+ );
+}
diff --git a/nextjs/src/i18n/client.ts b/nextjs/src/i18n/client.ts
new file mode 100644
index 0000000..eb6b1a7
--- /dev/null
+++ b/nextjs/src/i18n/client.ts
@@ -0,0 +1,22 @@
+'use client';
+
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+
+import en from './en.json';
+import ja from './ja.json';
+
+if (!i18n.isInitialized) {
+ i18n.use(initReactI18next).init({
+ fallbackLng: 'ja',
+ resources: {
+ en: { translation: en },
+ ja: { translation: ja },
+ },
+ interpolation: {
+ escapeValue: false,
+ },
+ });
+}
+
+export default i18n;
diff --git a/nextjs/src/i18n/en.json b/nextjs/src/i18n/en.json
new file mode 100644
index 0000000..ee82983
--- /dev/null
+++ b/nextjs/src/i18n/en.json
@@ -0,0 +1,9 @@
+{
+ "use_character": "Character",
+ "player_name": "Playername",
+ "description": "Description",
+ "archive": "Archive",
+ "guide": "Guide",
+ "about": "About",
+ "qanda": "Q&A"
+}
\ No newline at end of file
diff --git a/nextjs/src/i18n/ja.json b/nextjs/src/i18n/ja.json
new file mode 100644
index 0000000..f8f9ec6
--- /dev/null
+++ b/nextjs/src/i18n/ja.json
@@ -0,0 +1,9 @@
+{
+ "use_character": "使用キャラクター",
+ "player_name": "プレイヤー名",
+ "description": "紹介文",
+ "archive": "Archive",
+ "guide": "Guide",
+ "about": "About",
+ "qanda": "Q&A"
+}
\ No newline at end of file
diff --git a/nextjs/src/lib/data.ts b/nextjs/src/lib/data.ts
new file mode 100644
index 0000000..fead796
--- /dev/null
+++ b/nextjs/src/lib/data.ts
@@ -0,0 +1,74 @@
+import type { RowDataPacket } from 'mysql2';
+import { getPool } from './db';
+import type { IContactQuestion } from '@/models/IContactQuestion';
+import type { IGuideItem } from '@/models/IGuideItem';
+import type { IPlayerInfo } from '@/models/IPlayerInfo';
+
+let tournamentIdByKeyPromise: Promise> | null = null;
+
+async function loadTournamentMap(): Promise> {
+ const pool = getPool();
+ const [rows] = await pool.query(
+ 'SELECT id, tournament_key FROM tournaments'
+ );
+ const map: Record = {};
+ for (const row of rows) {
+ map[row.tournament_key as string] = row.id as number;
+ }
+ return map;
+}
+
+export async function getTournamentIdByKey(): Promise> {
+ if (!tournamentIdByKeyPromise) {
+ tournamentIdByKeyPromise = loadTournamentMap();
+ }
+ return tournamentIdByKeyPromise;
+}
+
+export async function getAllPlayers(): Promise {
+ const pool = getPool();
+ const [rows] = await pool.query(
+ `SELECT p.id, p.player_key, p.player_name, p.description, p.image, c.name_id, c.name, c.name_jp
+ FROM players p
+ JOIN characters c ON c.id = p.character_id`
+ );
+ return rows as IPlayerInfo[];
+}
+
+export async function getPlayersForTournament(
+ tournamentKey: string
+): Promise {
+ const tournaments = await getTournamentIdByKey();
+ const tournamentId = tournaments[tournamentKey];
+ if (tournamentId === undefined) {
+ return null;
+ }
+ const pool = getPool();
+ const [rows] = await pool.query(
+ `SELECT p.id, p.player_key, p.player_name, p.description, p.image, c.name_id, c.name, c.name_jp
+ FROM players p
+ JOIN player_to_tournament ptt ON p.id = ptt.player_id
+ JOIN characters c ON c.id = p.character_id
+ WHERE ptt.tournament_id = ?`,
+ [tournamentId]
+ );
+ return rows as IPlayerInfo[];
+}
+
+export async function getGuideItems(): Promise {
+ const pool = getPool();
+ const [rows] = await pool.query('SELECT * FROM guide');
+ return rows as IGuideItem[];
+}
+
+export async function getArchiveItems(): Promise {
+ const pool = getPool();
+ const [rows] = await pool.query('SELECT * FROM archive');
+ return rows as IGuideItem[];
+}
+
+export async function getContactItems(): Promise {
+ const pool = getPool();
+ const [rows] = await pool.query('SELECT * FROM contact');
+ return rows as IContactQuestion[];
+}
diff --git a/nextjs/src/lib/db.ts b/nextjs/src/lib/db.ts
new file mode 100644
index 0000000..4fbbbef
--- /dev/null
+++ b/nextjs/src/lib/db.ts
@@ -0,0 +1,23 @@
+import mysql from 'mysql2/promise';
+
+function createPool(): mysql.Pool {
+ const port = process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : 3306;
+ return mysql.createPool({
+ connectionLimit: 10,
+ port,
+ host: process.env.DB_ENDPOINT,
+ user: process.env.DB_USER,
+ password: process.env.DB_PASS,
+ database: 'catherine_league',
+ charset: 'utf8mb4',
+ });
+}
+
+let pool: mysql.Pool | undefined;
+
+export function getPool(): mysql.Pool {
+ if (!pool) {
+ pool = createPool();
+ }
+ return pool;
+}
diff --git a/nextjs/src/models/IArchiveItem.ts b/nextjs/src/models/IArchiveItem.ts
new file mode 100644
index 0000000..ac4d834
--- /dev/null
+++ b/nextjs/src/models/IArchiveItem.ts
@@ -0,0 +1,5 @@
+export interface IArchiveItem {
+ id: number;
+ title: string;
+ youtube_id: string;
+}
diff --git a/nextjs/src/models/IContactQuestion.ts b/nextjs/src/models/IContactQuestion.ts
new file mode 100644
index 0000000..0107fb9
--- /dev/null
+++ b/nextjs/src/models/IContactQuestion.ts
@@ -0,0 +1,5 @@
+export interface IContactQuestion {
+ id: number;
+ question: string;
+ answer: string;
+}
diff --git a/nextjs/src/models/IGuideItem.ts b/nextjs/src/models/IGuideItem.ts
new file mode 100644
index 0000000..9012fc4
--- /dev/null
+++ b/nextjs/src/models/IGuideItem.ts
@@ -0,0 +1,6 @@
+export interface IGuideItem {
+ id: number;
+ title: string;
+ description: string;
+ youtube_id: string;
+}
diff --git a/nextjs/src/models/IPlayerInfo.ts b/nextjs/src/models/IPlayerInfo.ts
new file mode 100644
index 0000000..a729655
--- /dev/null
+++ b/nextjs/src/models/IPlayerInfo.ts
@@ -0,0 +1,10 @@
+export interface IPlayerInfo {
+ id: number;
+ player_key: string;
+ player_name: string;
+ description: string;
+ image: string;
+ name_id: string;
+ name: string;
+ name_jp: string;
+}
diff --git a/nextjs/src/styles/web.module.scss b/nextjs/src/styles/web.module.scss
new file mode 100644
index 0000000..c8a6b1a
--- /dev/null
+++ b/nextjs/src/styles/web.module.scss
@@ -0,0 +1,600 @@
+.header {
+ background-image: url('https://static.catherine-fc.com/media/bgbg.png');
+ background-repeat: none;
+ width: 100%;
+ height: 460px;
+}
+.buynow {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ margin: auto;
+ width: 235px;
+ height: 99px;
+ background-image: url('https://static.catherine-fc.com/media/buynow.png');
+ z-index: 1;
+ &> a {
+ display:block;
+ width: 235px;
+ height: 99px;
+ }
+ &:hover {
+ animation: bounce 1s;
+ }
+}
+.background {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/bgimage.png');
+ background-repeat: none;
+ width: 1366px;
+ height: 401px;
+ animation: upAndDown 10s linear infinite;
+ transition: background-image 0.5s ease-in-out;
+ &:hover {
+ transition: background-image 0.5s ease-in-out;
+ background-image: url('https://static.catherine-fc.com/media/bgimagenight.png');
+ }
+}
+.logoHeader {
+ position: absolute;
+ top: -30px;
+ left: 0;
+ right: 0;
+ margin: auto;
+ width: 577px;
+ height: 401px;
+ background-image: url('https://static.catherine-fc.com/media/cat_fb_logo.png');
+ background-repeat: none;
+ z-index: 1;
+}
+
+.logoSwitch {
+ position: absolute;
+ top: 310px;
+ left: 0;
+ right: 0;
+ margin: auto;
+ width: 415px;
+ height: 64px;
+ background-image: url('https://static.catherine-fc.com/media/faito_crab.png');
+ background-repeat: none;
+ z-index: 1;
+}
+.contents {
+ flex-grow: 1;
+ padding: 30px;
+}
+.main {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ background-image: url('https://static.catherine-fc.com/media/background.png');
+ background-repeat: none;
+ color: white;
+ min-height: 100vh;
+}
+.footer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+ padding: 20px;
+ width: 100%;
+ background-color: black;
+ color: white;
+ bottom: 0;
+}
+.footerText {
+ float: left;
+ display: block;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+ & > p {
+ margin: 5px 0;
+ }
+}
+
+.twitch {
+ float: right;
+ width: 113px;
+ height: 70px;
+ background-image: url('https://static.catherine-fc.com/media/twitch.png');
+ background-size: 142px 70px;
+ margin: 0 20px;
+ background-repeat: none;
+ background-size: contain;
+ &> a {
+ display:block;
+ width: 113px;
+ height: 70px;
+ }
+}
+.twitchHome {
+ width: 113px;
+ height: 70px;
+ background-image: url('https://static.catherine-fc.com/media/twitch.png');
+ background-size: 142px 70px;
+ margin: auto;
+ background-repeat: none;
+ background-size: contain;
+ &> a {
+ display:block;
+ width: 113px;
+ height: 70px;
+ }
+}
+.twitter {
+ float: right;
+ width: 70px;
+ height: 70px;
+ margin: 0 20px;
+ background-image: url('https://static.catherine-fc.com/media/twitter.png');
+ background-size: contain;
+ &> a {
+ display:block;
+ width: 70px;
+ height: 70px;
+ }
+}
+.youtube {
+ float: right;
+ width: 100px;
+ height: 70px;
+ margin: 0 20px;
+ background-image: url('https://static.catherine-fc.com/media/youtube.png');
+ background-size: contain;
+ &> a {
+ display:block;
+ width: 100px;
+ height: 70px;
+ }
+}
+
+.waveContainer {
+ position: absolute;
+ top: 260px;
+ left: 0;
+ right: 0;
+ margin: auto;
+ z-index: 0;
+}
+.waves {
+ position: relative;
+ width: 100%;
+ height: 200px;
+ margin-bottom: -7px;
+
+ min-height: 100px;
+ max-height: 200px;
+}
+.firstWave {
+ animation: moveforever 25s cubic-bezier(.55, .5, .45, .5) infinite;
+ animation-delay: -2s;
+ animation-duration: 2s;
+}
+.secondWave {
+ animation: moveforever 25s cubic-bezier(.55, .5, .45, .5) infinite;
+ animation-delay: -4s;
+ animation-duration: 5s;
+}
+@keyframes moveforever {
+ 0% {
+ transform: translate3d(-90px, 0, 0);
+ }
+ 100% {
+ transform: translate3d(85px, 0, 0);
+ }
+}
+@keyframes bounce {
+ 0%, 20%, 60%, 100% {
+ -webkit-transform: translateY(0);
+ transform: translateY(0);
+ }
+
+ 40% {
+ -webkit-transform: translateY(-20px);
+ transform: translateY(-20px);
+ }
+
+ 80% {
+ -webkit-transform: translateY(-10px);
+ transform: translateY(-10px);
+ }
+}
+
+.navigation {
+ position: absolute;
+ top: 400px;
+ left: 0;
+ right: 0;
+ margin: auto;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ &>ul {
+ list-style-type: none;
+ &>li {
+ float: left;
+ margin-left: 10px;
+ margin-right: 10px;
+ &>a {
+ background-color: black;
+ width: 90px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-size: 16px;
+ font-family: var(--font-indie), cursive;
+ color: white;
+ &:hover {
+ transform: rotate(-10deg);
+ font-weight: bold;
+ color: pink;
+ }
+ }
+ }
+ }
+}
+.characterBlock {
+ position: relative;
+ opacity: 0;
+ width: 980px;
+ height: 314px;
+ margin: 20px auto;
+ padding: 30px;
+ background-image: url('https://static.catherine-fc.com/media/playerbg.png');
+ background-repeat: none;
+ font-family: var(--font-mplus), sans-serif;
+ &:nth-child(odd) {
+ animation: slideInFromLeft 1s forwards;
+ }
+ &:nth-child(even) {
+ animation: slideInFromRight 1s forwards;
+ }
+}
+@keyframes slideInFromLeft {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+@keyframes slideInFromRight {
+ 0% {
+ transform: translateX(100%);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+@keyframes upAndDown {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-5%);
+ }
+}
+@media (max-width: 600px) {
+ .buynow {
+ display: none;
+ }
+}
+
+@mixin characterImage($url) {
+ width: 240px;
+ height: 240px;
+ margin: 0 20px 0 0;
+ background-image: url($url);
+ background-size: contain;
+ float: left;
+
+}
+
+.character-blue_cap {
+ @include characterImage('https://static.catherine-fc.com/media/00_bluecap_1204.png');
+}
+.character-red_cap {
+ @include characterImage('https://static.catherine-fc.com/media/00_redcap_1204.png');
+}
+.character-vincent_shirt {
+ @include characterImage('https://static.catherine-fc.com/media/01_vincent_1204.png');
+}
+.character-vincent_sheep {
+ @include characterImage('https://static.catherine-fc.com/media/01_sheep_vincent_1204.png');
+}
+.character-katherine {
+ @include characterImage('https://static.catherine-fc.com/media/02_katherine_1204.png');
+}
+.character-catherine {
+ @include characterImage('https://static.catherine-fc.com/media/03_catherine_1204.png');
+}
+.character-rin {
+ @include characterImage('https://static.catherine-fc.com/media/04_rin_1204.png');
+}
+.character-orlando {
+ @include characterImage('https://static.catherine-fc.com/media/05_orlando_1204.png');
+}
+.character-johny {
+ @include characterImage('https://static.catherine-fc.com/media/06_jonny_1204.png');
+}
+.character-tobby {
+ @include characterImage('https://static.catherine-fc.com/media/07_tobby_1204.png');
+}
+.character-erica {
+ @include characterImage('https://static.catherine-fc.com/media/08_erika_1204.png');
+}
+.character-master {
+ @include characterImage('https://static.catherine-fc.com/media/09_master_1204.png');
+}
+.character-joker {
+ @include characterImage('https://static.catherine-fc.com/media/13_joker_1204.png');
+}
+
+.titlePlayer {
+ position: relative;
+ display: flex;
+ margin-bottom: 10px;
+}
+
+.titlePlayerBlock {
+ padding: 5px 10px;
+ background-color: white;
+ transform: rotate(-5deg);
+ color: black;
+ font-family: var(--font-mplus), sans-serif;
+}
+
+.aboutNoticeBlock {
+ position: relative;
+ width: 980px;
+ height: 800px;
+ margin: 20px auto;
+ padding: 50px;
+ background-image: url('https://static.catherine-fc.com/media/playerbg.png');
+ background-repeat: none;
+ background-size: 980px 800px;
+ font-family: var(--font-mplus), sans-serif;
+}
+
+.sheepAbout {
+ position: relative;
+ width: 207px;
+ height: 350px;
+ float: left;
+ margin-top: 70px;
+ bottom: 0;
+ left: 0;
+ background-image: url('https://static.catherine-fc.com/media/title_sheep.png');
+ background-repeat: none;
+}
+.guideBody {
+ position: relative;
+ margin: 20px auto;
+ width: 980px;
+}
+.centerVideo {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+}
+
+.aboutContactBox {
+ position: relative;
+ width: 128px;
+ height: 128px;
+ margin: 20px;
+ background-image: url('https://static.catherine-fc.com/media/letterbox.png');
+ background-size: 128px 128px;
+ background-repeat: none;
+ & > a {
+ display: block;
+ width: 128px;
+ height: 128px;
+ }
+}
+.contactBox {
+ position: relative;
+ width: 128px;
+ height: 128px;
+ margin: 20px auto;
+ background-image: url('https://static.catherine-fc.com/media/letterbox.png');
+ background-size: 128px 128px;
+ background-repeat: none;
+ & > a {
+ display: block;
+ width: 128px;
+ height: 128px;
+ }
+}
+
+.mainBody {
+ position: flex;
+ margin: 20px auto;
+ width: 980px;
+ align-items: center;
+ justify-content: center;
+}
+.chalice {
+ width: 400px;
+ height: 340px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/chalice.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.evojapan2023 {
+ width: 500px;
+ height: 471px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/evo_logo_shadow_drop.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.evojapan2024 {
+ width: 500px;
+ height: 471px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/evo_2024_logo_shadow_drop.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.evojapan2025 {
+ width: 500px;
+ height: 471px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/evo_logo_2025_shadow_drop.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.evojapan2023kaisaikettei {
+ width: 846px;
+ height: 285px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/kaisaikettei3.png');
+ background-size: contain;
+}
+.evojapan2023catherinetonamel {
+ width: 412px;
+ height: 389px;
+ background-image: url('https://static.catherine-fc.com/media/catherine_logo_classic.png');
+ background-size: contain;
+ background-repeat: none;
+ &> a {
+ display:block;
+ width: 412px;
+ height: 389px;
+ }
+}
+.evojapan2023catherinefullbodytonamel {
+ width: 517px;
+ height: 319px;
+ background-image: url('https://static.catherine-fc.com/media/catherine_fullbody_logo.png');
+ background-size: contain;
+ background-repeat: none;
+ &> a {
+ display:block;
+ width: 517px;
+ height: 319px;
+ }
+}
+.group {
+ width: 900px;
+ height: 389px;
+ margin: auto;
+ display: flex;
+ background-size: contain;
+ background-repeat: none;
+}
+.players {
+ width: 900px;
+ height: 606px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/players.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.scoreboardImage {
+ width: 567px;
+ height: 485px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/scoreboard.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.titleImageStrayShip0 {
+ width: 808px;
+ height: 119px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/straysheepcup0.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.titleImageStrayShip4 {
+ width: 808px;
+ height: 119px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/straysheepcup4.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.rule {
+ width: 900px;
+ height: 637px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/rule.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.tonamel {
+ width: 900px;
+ height: 637px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/tonamel.png');
+ background-size: contain;
+ background-repeat: none;
+ &> a {
+ display:block;
+ width: 900px;
+ height: 637px;
+ }
+}
+.padding {
+ width: 900px;
+ height: 100px;
+ margin: auto;
+}
+.players0801 {
+ width: 899px;
+ height: 696px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/0801.png');
+ background-size: contain;
+ background-repeat: none;
+}
+
+.players0802 {
+ width: 900px;
+ height: 710px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/0802.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.players0808 {
+ width: 900px;
+ height: 710px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/0808.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.players0809 {
+ width: 900px;
+ height: 696px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/0809.png');
+ background-size: contain;
+ background-repeat: none;
+}
+.players0815 {
+ width: 900px;
+ height: 696px;
+ margin: auto;
+ background-image: url('https://static.catherine-fc.com/media/0815.png');
+ background-size: contain;
+ background-repeat: none;
+}
diff --git a/nextjs/tsconfig.json b/nextjs/tsconfig.json
new file mode 100644
index 0000000..f48e7ee
--- /dev/null
+++ b/nextjs/tsconfig.json
@@ -0,0 +1,40 @@
+{
+ "compilerOptions": {
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ },
+ "target": "ES2017"
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..125d8b1
--- /dev/null
+++ b/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "catherine-league",
+ "private": true,
+ "workspaces": [
+ "nextjs"
+ ]
+}