Play page initial (#700)

This commit is contained in:
Kalle 2022-01-27 00:03:03 +02:00 committed by GitHub
parent 54eff4747b
commit 8e3a96d0c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1440 additions and 3706 deletions

3
.gitignore vendored
View File

@ -5,3 +5,6 @@ node_modules
/server/build
/public/build
/app/tailwind.css
db.sqlite3
db.sqlite3-shm
db.sqlite3-wal

View File

@ -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>

View 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>
</>
);
}

View 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,
},
},
},
});
}

View File

@ -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
View 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>
);
}

View File

@ -1,5 +0,0 @@
.beta__container {
max-width: 48rem;
margin: 0 auto;
padding-inline: var(--s-2);
}

View File

@ -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
View 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);
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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;

View File

@ -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[]
}