diff --git a/components/builds/BuildCard.tsx b/components/builds/BuildCard.tsx index b28453190..4bc4d590e 100644 --- a/components/builds/BuildCard.tsx +++ b/components/builds/BuildCard.tsx @@ -13,7 +13,7 @@ import { Ability } from "@prisma/client"; import UserAvatar from "components/common/UserAvatar"; import WeaponImage from "components/common/WeaponImage"; import { getEmojiFlag } from "countries-list"; -import { Unpacked } from "lib/types"; +import { PartialBy, Unpacked } from "lib/types"; import { useMyTheme } from "lib/useMyTheme"; import Image from "next/image"; import Link from "next/link"; @@ -26,7 +26,7 @@ import ViewAP from "./ViewAP"; import ViewSlots from "./ViewSlots"; interface BuildCardProps { - build: Unpacked>; + build: PartialBy>, "user">; canModify?: boolean; //setBuildBeingEdited?: (build: Build) => void; otherBuildCount?: number; @@ -48,7 +48,7 @@ const BuildCard: React.FC = ({ const { themeColorShade, secondaryBgColor, gray } = useMyTheme(); - const username = build.user.username; + const username = build.user?.username; return ( <> @@ -97,6 +97,11 @@ const BuildCard: React.FC = ({ )} + {build.weapon && ( + + + + )} +>; + interface GearsProps { - build: Unpacked>; + build: BuildViewSlots; } const Gears: React.FC = ({ build }) => { diff --git a/components/builds/ViewSlots.tsx b/components/builds/ViewSlots.tsx index e0aa86c12..52dadfe02 100644 --- a/components/builds/ViewSlots.tsx +++ b/components/builds/ViewSlots.tsx @@ -1,10 +1,19 @@ import { Box, BoxProps, Flex } from "@chakra-ui/react"; +import { BuildGetPayload } from "@prisma/client"; import AbilityIcon from "components/common/AbilityIcon"; -import { Unpacked } from "lib/types"; -import { GetBuildsByWeaponData } from "prisma/queries/getBuildsByWeapon"; + +type BuildViewSlots = Partial< + BuildGetPayload<{ + select: { + headAbilities: true; + clothingAbilities: true; + shoesAbilities: true; + }; + }> +>; interface ViewSlotsProps { - build: Unpacked>; + build: BuildViewSlots; onAbilityClick?: (gear: "HEAD" | "CLOTHING" | "SHOES", index: number) => void; } diff --git a/components/u/AvatarWithInfo.tsx b/components/u/AvatarWithInfo.tsx index a62154a57..61236bb03 100644 --- a/components/u/AvatarWithInfo.tsx +++ b/components/u/AvatarWithInfo.tsx @@ -132,7 +132,7 @@ const AvatarWithInfo: React.FC = ({ alignItems="center" justifyContent="center" ml="0.7rem" - mt="0.7rem" + mt={1} color={gray} w="100%" > @@ -144,7 +144,7 @@ const AvatarWithInfo: React.FC = ({ )} {Object.keys(peakXPowers).length > 0 && ( - + {(["SZ", "TC", "RM", "CB"] as RankedMode[]).map((mode, i) => ( {peakXPowers[mode] && ( diff --git a/hooks/u.ts b/hooks/u.ts new file mode 100644 index 000000000..79df8632b --- /dev/null +++ b/hooks/u.ts @@ -0,0 +1,29 @@ +import { GetBuildsByUserData } from "prisma/queries/getBuildsByUser"; +import { useState } from "react"; +import useSWR from "swr"; + +export function useBuildsByUser(userId?: number) { + const [weapon, setWeapon] = useState(null); + + const { data = [] } = useSWR( + `/api/users/${userId}/builds` + ); + + const weaponCounts = data.reduce((acc: [string, number][], build) => { + const foundTuple = acc.find((tuple) => tuple[0] === build.weapon); + if (foundTuple) { + foundTuple[1] = foundTuple[1] + 1; + return acc; + } + + acc.push([build.weapon, 1]); + return acc; + }, []); + + return { + data: weapon ? data.filter((build) => build.weapon === weapon) : data, + weaponCounts, + setWeapon, + buildCount: data.length, + }; +} diff --git a/lib/types.ts b/lib/types.ts index 247640478..7379a6ea5 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1 +1,2 @@ export type Unpacked = T extends (infer U)[] ? U : T; +export type PartialBy = Omit & Partial>; diff --git a/old/README.md b/old/README.md index a7206bce7..0be78f301 100644 --- a/old/README.md +++ b/old/README.md @@ -1,21 +1,17 @@ [![Discord Server](https://discordapp.com/api/guilds/299182152161951744/embed.png)](https://discord.gg/sendou) -Goal of sendou.ink is to provide useful tools and resources for the competitive Splatoon community. +Goal of sendou.ink is to provide useful tools and resources for the Splatoon community. Live version: [https://sendou.ink/](https://sendou.ink/) -## What's happening - -This site was originally a full stack web development school project. Since I have continued working on it as a hobby. I use a big portion of the tools offered by it myself so finding continued motivation has been easy. +> :warning: **This is the overhaul branch**: Live version is still on the old version ## Technologies used -- React +- React (via Next.JS) - TypeScript - Node.js -- GraphQL (Apollo Server) -- MongoDB (+Mongoose) -- Python (a couple of different scripts to parse data) +- PostgreSQL (via Prisma 2) ## A few highlight features @@ -31,26 +27,12 @@ This site was originally a full stack web development school project. Since I ha 🦑 Browse through detailed tournament results -🦑 Choose between light and dark mode as well as 10 different accent colors - -## GraphQL - -Serving the site is a GraphQL API. You can explore the schema here: [https://sendou.ink/graphql](https://sendou.ink/graphql). It was made specifically for this site but you are free to use it within reason if it is of use to you. +🦑 Choose between light and dark mode ## Installation & getting started -1. Install [Node](https://nodejs.org/en/) -2. Use the `npm run install:all` command in the root folder -3. Make a copy of the `.env.template` file and rename it to `.env`. Populate the values depending on what you are developing. Database URI's at least are necessary to spin up the backend. -4. Use the `npm run dev` command in the root folder to develop logged out and `npm run dev:loggedin` to develop logged in as a mocked user. - -- Please note that the scripts are currently assuming an \*NIX based system so trying to run them on PowerShell or something might not work. - -Making the installation process smoother is one big point of improvement hopefully in the near future. - -Server will run on [http://localhost:3001/graphql](http://localhost:3001/) -Front-end will run on [http://localhost:3000/](http://localhost:3000/) +TODO ## Contributing -You are welcome to create an issue or do a pull request. +TODO diff --git a/pages/api/users/[id]/builds.ts b/pages/api/users/[id]/builds.ts new file mode 100644 index 000000000..9dc64b2e0 --- /dev/null +++ b/pages/api/users/[id]/builds.ts @@ -0,0 +1,20 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { + getBuildsByUser, + GetBuildsByUserData, +} from "prisma/queries/getBuildsByUser"; + +const usersBuildsHandler = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + if (req.method !== "GET") { + return res.status(405).end(); + } + + const builds = await getBuildsByUser(Number(req.query.id)); + + res.status(200).json(builds); +}; + +export default usersBuildsHandler; diff --git a/pages/api/users/[identifier].ts b/pages/api/users/[id]/index.ts similarity index 85% rename from pages/api/users/[identifier].ts rename to pages/api/users/[id]/index.ts index 3fdb41e6a..50a54eb50 100644 --- a/pages/api/users/[identifier].ts +++ b/pages/api/users/[id]/index.ts @@ -12,7 +12,7 @@ const userHandler = async ( return res.status(405).end(); } - const user = await getUserByIdentifier(req.query.identifier as string); + const user = await getUserByIdentifier(req.query.id as string); if (!user) return res.status(404).end(); res.status(200).json(user); }; diff --git a/pages/u/[identifier].tsx b/pages/u/[identifier].tsx index 1d6c3d1b2..39d82909a 100644 --- a/pages/u/[identifier].tsx +++ b/pages/u/[identifier].tsx @@ -1,10 +1,13 @@ -import { Box, Button, Divider } from "@chakra-ui/react"; +import { Button, Divider, Select } from "@chakra-ui/react"; import { t, Trans } from "@lingui/macro"; import { RankedMode } from "@prisma/client"; +import BuildCard from "components/builds/BuildCard"; import Breadcrumbs from "components/common/Breadcrumbs"; import Markdown from "components/common/Markdown"; +import MyInfiniteScroller from "components/common/MyInfiniteScroller"; import AvatarWithInfo from "components/u/AvatarWithInfo"; import ProfileModal from "components/u/ProfileModal"; +import { useBuildsByUser } from "hooks/u"; import { getFullUsername } from "lib/strings"; import useUser from "lib/useUser"; import { GetStaticPaths, GetStaticProps } from "next"; @@ -86,6 +89,9 @@ const ProfilePage = (props: Props) => { }, { initialData: props.user } ); + const { data: builds, weaponCounts, setWeapon, buildCount } = useBuildsByUser( + user?.id + ); // same as router.isFallback // FIXME: return spinner @@ -108,11 +114,38 @@ const ProfilePage = (props: Props) => { {showModal && ( setShowModal(false)} user={user} /> )} - {user.profile?.bio && ( - + <> + - + + )} + {buildCount > 0 && ( + <> + + {buildCount > 6 && ( + + )} + + {builds.map((build) => ( + + ))} + + )} ); diff --git a/prisma/queries/getBuildsByUser.ts b/prisma/queries/getBuildsByUser.ts new file mode 100644 index 000000000..62aa7d641 --- /dev/null +++ b/prisma/queries/getBuildsByUser.ts @@ -0,0 +1,12 @@ +import { PromiseReturnType } from "@prisma/client"; +import DBClient from "prisma/client"; + +export type GetBuildsByUserData = PromiseReturnType; + +const prisma = DBClient.getInstance().prisma; + +export const getBuildsByUser = async (userId: number) => + prisma.build.findMany({ + where: { userId }, + orderBy: [{ updatedAt: "desc" }], + }); diff --git a/prisma/queries/getUserByIdentifier.ts b/prisma/queries/getUserByIdentifier.ts index 4b4476fe6..d0e941d59 100644 --- a/prisma/queries/getUserByIdentifier.ts +++ b/prisma/queries/getUserByIdentifier.ts @@ -20,6 +20,9 @@ export const getUserByIdentifier = (identifier: string) => customUrlPath: identifier.toLowerCase(), }, }, + { + id: Number(identifier), + }, ], }, select: {