mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Play page initial (#700)
This commit is contained in:
parent
54eff4747b
commit
8e3a96d0c1
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -5,3 +5,6 @@ node_modules
|
|||
/server/build
|
||||
/public/build
|
||||
/app/tailwind.css
|
||||
db.sqlite3
|
||||
db.sqlite3-shm
|
||||
db.sqlite3-wal
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { SearchInput } from "./SearchInput";
|
|||
import { UserItem } from "./UserItem";
|
||||
import { Link } from "remix";
|
||||
import { navItems } from "~/constants";
|
||||
import { layoutIcon } from "~/utils";
|
||||
|
||||
export const Layout = React.memo(function Layout({
|
||||
children,
|
||||
|
|
@ -19,7 +20,7 @@ export const Layout = React.memo(function Layout({
|
|||
<header className="layout__header">
|
||||
<div className="layout__header__logo-container">
|
||||
<Link to="/">
|
||||
<img className="layout__logo" src="/img/layout/logo.webp" />
|
||||
<img className="layout__logo" src={layoutIcon("logo")} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="layout__header__search-container">
|
||||
|
|
@ -50,8 +51,10 @@ export const Layout = React.memo(function Layout({
|
|||
data-cy={`nav-link-${navItem}`}
|
||||
>
|
||||
<img
|
||||
src={`/img/layout/${navItem.replace(" ", "")}.webp`}
|
||||
src={layoutIcon(navItem.replace(" ", ""))}
|
||||
className="layout__nav__link__icon"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
{navItem}
|
||||
</Link>
|
||||
|
|
|
|||
76
app/components/play/LFGGroupSelector.tsx
Normal file
76
app/components/play/LFGGroupSelector.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import * as React from "react";
|
||||
import { RadioGroup } from "@headlessui/react";
|
||||
import { layoutIcon } from "~/utils";
|
||||
import clsx from "clsx";
|
||||
|
||||
const OPTIONS = [
|
||||
{
|
||||
type: "VERSUS-RANKED",
|
||||
image: "rotations",
|
||||
text: "Versus",
|
||||
explanation: "Private Battle (ranked)",
|
||||
},
|
||||
{
|
||||
type: "VERSUS-UNRANKED",
|
||||
image: "rotations",
|
||||
text: "Versus",
|
||||
explanation: "Private Battle (unranked)",
|
||||
},
|
||||
{
|
||||
type: "TWIN",
|
||||
image: "rotations",
|
||||
text: "Twin",
|
||||
explanation: "League Battle",
|
||||
},
|
||||
{
|
||||
type: "QUAD",
|
||||
image: "rotations",
|
||||
text: "Quad",
|
||||
explanation: "League Battle",
|
||||
},
|
||||
];
|
||||
|
||||
export function LFGGroupSelector() {
|
||||
const [type, setType] = React.useState("VERSUS-RANKED");
|
||||
|
||||
return (
|
||||
<>
|
||||
<input type="hidden" name="type" value={type} />
|
||||
<RadioGroup
|
||||
className="play__type-radio-group"
|
||||
value={type}
|
||||
onChange={setType}
|
||||
>
|
||||
{OPTIONS.map((option, i) => {
|
||||
return (
|
||||
<RadioGroup.Option key={i} value={option.type}>
|
||||
{({ checked }) => (
|
||||
<div
|
||||
className={clsx("play__type-radio-group__item", { checked })}
|
||||
>
|
||||
<label className="play__type-radio-group__label">
|
||||
{option.text}
|
||||
<span
|
||||
className={clsx(
|
||||
"play__type-radio-group__label__explanation",
|
||||
{ checked }
|
||||
)}
|
||||
>
|
||||
{option.explanation}
|
||||
</span>
|
||||
</label>
|
||||
<img
|
||||
className={clsx("play__type-radio-group__image", {
|
||||
checked,
|
||||
})}
|
||||
src={layoutIcon(option.image)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
28
app/models/LFGGroup.server.ts
Normal file
28
app/models/LFGGroup.server.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import type { LFGGroupType } from "@prisma/client";
|
||||
import { db } from "~/utils/db.server";
|
||||
|
||||
export function create({
|
||||
type,
|
||||
ranked,
|
||||
user,
|
||||
}: {
|
||||
type: LFGGroupType;
|
||||
ranked?: boolean;
|
||||
user: { id: string };
|
||||
}) {
|
||||
return db.lFGGroup.create({
|
||||
data: {
|
||||
type,
|
||||
// TWIN becomes active immediately because it makes no sense
|
||||
// to pre-add players to the group
|
||||
active: type === "TWIN",
|
||||
ranked,
|
||||
members: {
|
||||
create: {
|
||||
memberId: user.id,
|
||||
captain: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1,14 +1,8 @@
|
|||
import type { LinksFunction } from "remix";
|
||||
import { DISCORD_URL } from "~/constants";
|
||||
import styles from "~/styles/beta.css";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export default function BetaPage() {
|
||||
return (
|
||||
<div className="beta__container">
|
||||
<div className="container">
|
||||
<h2>Beta of sendou.ink (Splatoon 3)</h2>
|
||||
<p>
|
||||
Hello there! I appreciate you taking time to visit this beta version of
|
||||
|
|
|
|||
83
app/routes/play/index.tsx
Normal file
83
app/routes/play/index.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { ActionFunction, Form, LinksFunction, MetaFunction } from "remix";
|
||||
import { z } from "zod";
|
||||
import { LFGGroupSelector } from "~/components/play/LFGGroupSelector";
|
||||
import styles from "~/styles/play.css";
|
||||
import { makeTitle, parseRequestFormData, requireUser } from "~/utils";
|
||||
import * as LFGGroup from "~/models/LFGGroup.server";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
const playActionSchema = z.object({
|
||||
_action: z.literal("CREATE_LFG_GROUP"),
|
||||
type: z.enum(["VERSUS-RANKED", "VERSUS-UNRANKED", "TWIN", "QUAD"]),
|
||||
});
|
||||
|
||||
type ActionData = {
|
||||
ok?: z.infer<typeof playActionSchema>["_action"];
|
||||
};
|
||||
|
||||
export const action: ActionFunction = async ({
|
||||
request,
|
||||
context,
|
||||
}): Promise<ActionData> => {
|
||||
const data = await parseRequestFormData({
|
||||
request,
|
||||
schema: playActionSchema,
|
||||
});
|
||||
const user = requireUser(context);
|
||||
|
||||
switch (data._action) {
|
||||
case "CREATE_LFG_GROUP": {
|
||||
const getRanked = () => {
|
||||
if (!data.type.startsWith("VERSUS")) return;
|
||||
return data.type.includes("UNRANKED") ? false : true;
|
||||
};
|
||||
const getType = () => {
|
||||
switch (data.type) {
|
||||
case "VERSUS-RANKED":
|
||||
case "VERSUS-UNRANKED":
|
||||
return "VERSUS";
|
||||
case "QUAD":
|
||||
case "TWIN":
|
||||
return data.type;
|
||||
}
|
||||
};
|
||||
await LFGGroup.create({
|
||||
user,
|
||||
type: getType(),
|
||||
ranked: getRanked(),
|
||||
});
|
||||
|
||||
return { ok: "CREATE_LFG_GROUP" };
|
||||
}
|
||||
default: {
|
||||
const exhaustive: never = data._action;
|
||||
throw new Response(`Unknown action: ${JSON.stringify(exhaustive)}`, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return {
|
||||
title: makeTitle("Play!"),
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: loader: redirect to /lfg if active LFGGroup
|
||||
// redirect to /match if active LFGGroup AND match
|
||||
|
||||
export default function PlayPage() {
|
||||
return (
|
||||
<div className="container">
|
||||
<Form method="post">
|
||||
<input type="hidden" name="_action" value="CREATE_LFG_GROUP" />
|
||||
<LFGGroupSelector />
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
.beta__container {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
|
@ -391,6 +391,12 @@ select::selection {
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
|
||||
.mt-2 {
|
||||
|
|
|
|||
56
app/styles/play.css
Normal file
56
app/styles/play.css
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
.play__type-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.play__type-radio-group__item {
|
||||
display: flex;
|
||||
max-width: 24rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
background-color: var(--bg-lighter-transparent);
|
||||
border-radius: var(--rounded);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.play__type-radio-group__item:hover {
|
||||
background-color: var(--theme-transparent);
|
||||
}
|
||||
|
||||
.play__type-radio-group__item.checked {
|
||||
background-color: var(--theme);
|
||||
color: var(--button-text);
|
||||
}
|
||||
|
||||
.play__type-radio-group__label {
|
||||
display: grid;
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
grid-template-columns: 50px 1fr;
|
||||
margin-inline-start: var(--s-2);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.play__type-radio-group__label__explanation {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xs);
|
||||
margin-block-start: 1px;
|
||||
margin-inline-start: var(--s-2);
|
||||
}
|
||||
|
||||
.play__type-radio-group__label__explanation.checked {
|
||||
color: var(--button-text-transparent);
|
||||
}
|
||||
|
||||
.play__type-radio-group__image {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
.play__type-radio-group__image.checked {
|
||||
transform: scale(1.75);
|
||||
}
|
||||
|
|
@ -68,6 +68,10 @@ export function modeToImageUrl(mode: Mode) {
|
|||
return `/img/modes/${mode}.webp`;
|
||||
}
|
||||
|
||||
export function layoutIcon(icon: string) {
|
||||
return `/img/layout/${icon}.webp`;
|
||||
}
|
||||
|
||||
/** Parse formData of a request with the given schema. Throws HTTP 400 response if fails. */
|
||||
export async function parseRequestFormData<T extends z.ZodTypeAny>({
|
||||
request,
|
||||
|
|
@ -111,6 +115,8 @@ export type Serialized<T> = {
|
|||
: Serialized<T[P]>;
|
||||
};
|
||||
|
||||
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
export type Unpacked<T> = T extends (infer U)[]
|
||||
? U
|
||||
: T extends (...args: unknown[]) => infer U
|
||||
|
|
|
|||
4762
package-lock.json
generated
4762
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -14,6 +14,7 @@
|
|||
"migration:apply:dev": "npx prisma migrate dev",
|
||||
"migration:apply:prod": "npx prisma migrate deploy",
|
||||
"seed": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register prisma/seed",
|
||||
"seed2": "TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register app/db/seed",
|
||||
"seed:reset": "npx prisma migrate reset --force --skip-generate",
|
||||
"lint:ts": "eslint . --ext .ts,.tsx",
|
||||
"lint:styles": "stylelint \"app/styles/**/*.css\"",
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
"@dnd-kit/core": "^5.0.1",
|
||||
"@dnd-kit/sortable": "^6.0.0",
|
||||
"@dnd-kit/utilities": "^3.1.0",
|
||||
"@headlessui/react": "^1.4.3",
|
||||
"@prisma/client": "^3.8.1",
|
||||
"@remix-run/express": "^1.1.3",
|
||||
"@remix-run/react": "^1.1.3",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ CREATE TYPE "BracketType" AS ENUM ('SE', 'DE');
|
|||
-- CreateEnum
|
||||
CREATE TYPE "TeamOrder" AS ENUM ('UPPER', 'LOWER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LFGGroupType" AS ENUM ('TWIN', 'QUAD', 'VERSUS');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
|
|
@ -150,6 +153,40 @@ CREATE TABLE "TournamentMatchGameResult" (
|
|||
CONSTRAINT "TournamentMatchGameResult_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LFGGroup" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ranked" BOOLEAN,
|
||||
"type" "LFGGroupType" NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"matchId" TEXT,
|
||||
"inviteCode" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "LFGGroup_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LFGGroupLike" (
|
||||
"likerId" TEXT NOT NULL,
|
||||
"targetId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LFGGroupMember" (
|
||||
"groupId" TEXT NOT NULL,
|
||||
"memberId" TEXT NOT NULL,
|
||||
"captain" BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LFGGroupMatch" (
|
||||
"id" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "LFGGroupMatch_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_TournamentMatchGameResultToUser" (
|
||||
"A" TEXT NOT NULL,
|
||||
|
|
@ -192,6 +229,12 @@ CREATE UNIQUE INDEX "TournamentMatchParticipant_teamId_matchId_key" ON "Tourname
|
|||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TournamentMatchGameResult_matchId_roundStageId_key" ON "TournamentMatchGameResult"("matchId", "roundStageId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LFGGroupLike_likerId_targetId_key" ON "LFGGroupLike"("likerId", "targetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LFGGroupMember_groupId_memberId_key" ON "LFGGroupMember"("groupId", "memberId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_TournamentMatchGameResultToUser_AB_unique" ON "_TournamentMatchGameResultToUser"("A", "B");
|
||||
|
||||
|
|
@ -258,6 +301,21 @@ ALTER TABLE "TournamentMatchGameResult" ADD CONSTRAINT "TournamentMatchGameResul
|
|||
-- AddForeignKey
|
||||
ALTER TABLE "TournamentMatchGameResult" ADD CONSTRAINT "TournamentMatchGameResult_roundStageId_fkey" FOREIGN KEY ("roundStageId") REFERENCES "TournamentRoundStage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LFGGroup" ADD CONSTRAINT "LFGGroup_matchId_fkey" FOREIGN KEY ("matchId") REFERENCES "LFGGroupMatch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LFGGroupLike" ADD CONSTRAINT "LFGGroupLike_likerId_fkey" FOREIGN KEY ("likerId") REFERENCES "LFGGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LFGGroupLike" ADD CONSTRAINT "LFGGroupLike_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "LFGGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LFGGroupMember" ADD CONSTRAINT "LFGGroupMember_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "LFGGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LFGGroupMember" ADD CONSTRAINT "LFGGroupMember_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TournamentMatchGameResultToUser" ADD FOREIGN KEY ("A") REFERENCES "TournamentMatchGameResult"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ model User {
|
|||
ownedOrganization Organization?
|
||||
tournamentTeams TournamentTeamMember[]
|
||||
tournamentMatches TournamentMatchGameResult[]
|
||||
lfgGroups LFGGroupMember[]
|
||||
}
|
||||
|
||||
model Organization {
|
||||
|
|
@ -202,3 +203,48 @@ model TournamentMatchGameResult {
|
|||
|
||||
@@unique([matchId, roundStageId])
|
||||
}
|
||||
|
||||
enum LFGGroupType {
|
||||
TWIN
|
||||
QUAD
|
||||
VERSUS
|
||||
}
|
||||
|
||||
model LFGGroup {
|
||||
id String @id @default(uuid())
|
||||
ranked Boolean?
|
||||
type LFGGroupType
|
||||
active Boolean @default(true)
|
||||
matchId String?
|
||||
inviteCode String @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
members LFGGroupMember[]
|
||||
match LFGGroupMatch? @relation(fields: [matchId], references: [id])
|
||||
likedGroups LFGGroupLike[] @relation("liker")
|
||||
likesReceived LFGGroupLike[] @relation("target")
|
||||
}
|
||||
|
||||
model LFGGroupLike {
|
||||
likerId String
|
||||
targetId String
|
||||
liker LFGGroup @relation("liker", fields: [likerId], references: [id])
|
||||
target LFGGroup @relation("target", fields: [targetId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([likerId, targetId])
|
||||
}
|
||||
|
||||
model LFGGroupMember {
|
||||
groupId String
|
||||
memberId String
|
||||
captain Boolean @default(false)
|
||||
group LFGGroup @relation(fields: [groupId], references: [id])
|
||||
user User @relation(fields: [memberId], references: [id])
|
||||
|
||||
@@unique([groupId, memberId])
|
||||
}
|
||||
|
||||
model LFGGroupMatch {
|
||||
id String @id @default(uuid())
|
||||
groups LFGGroup[]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user