builds on profile page

This commit is contained in:
Kalle (Sendou) 2020-11-18 12:49:33 +02:00
parent 8a10eb265d
commit 03af9fa7bb
12 changed files with 144 additions and 41 deletions

View File

@ -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<Unpacked<GetBuildsByWeaponData>>;
build: PartialBy<Unpacked<Unpacked<GetBuildsByWeaponData>>, "user">;
canModify?: boolean;
//setBuildBeingEdited?: (build: Build) => void;
otherBuildCount?: number;
@ -48,7 +48,7 @@ const BuildCard: React.FC<BuildCardProps & BoxProps> = ({
const { themeColorShade, secondaryBgColor, gray } = useMyTheme();
const username = build.user.username;
const username = build.user?.username;
return (
<>
@ -97,6 +97,11 @@ const BuildCard: React.FC<BuildCardProps & BoxProps> = ({
</Link>
</Box>
)}
{build.weapon && (
<Box>
<WeaponImage name={build.weapon} size={64} />
</Box>
)}
<Flex alignItems="center">
<Box
color={gray}

View File

@ -1,11 +1,20 @@
import { Box, Flex } from "@chakra-ui/react";
import { BuildGetPayload } from "@prisma/client";
import GearImage from "components/common/GearImage";
import { Unpacked } from "lib/types";
import { GetBuildsByWeaponData } from "prisma/queries/getBuildsByWeapon";
import React from "react";
type BuildViewSlots = Partial<
BuildGetPayload<{
select: {
headGear: true;
clothingGear: true;
shoesGear: true;
};
}>
>;
interface GearsProps {
build: Unpacked<Unpacked<GetBuildsByWeaponData>>;
build: BuildViewSlots;
}
const Gears: React.FC<GearsProps> = ({ build }) => {

View File

@ -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<Unpacked<GetBuildsByWeaponData>>;
build: BuildViewSlots;
onAbilityClick?: (gear: "HEAD" | "CLOTHING" | "SHOES", index: number) => void;
}

View File

@ -132,7 +132,7 @@ const AvatarWithInfo: React.FC<AvatarWithInfoProps> = ({
alignItems="center"
justifyContent="center"
ml="0.7rem"
mt="0.7rem"
mt={1}
color={gray}
w="100%"
>
@ -144,7 +144,7 @@ const AvatarWithInfo: React.FC<AvatarWithInfoProps> = ({
</Flex>
)}
{Object.keys(peakXPowers).length > 0 && (
<Flex mt={6}>
<Flex mt={4}>
{(["SZ", "TC", "RM", "CB"] as RankedMode[]).map((mode, i) => (
<Fragment key={mode}>
{peakXPowers[mode] && (

29
hooks/u.ts Normal file
View File

@ -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<string | null>(null);
const { data = [] } = useSWR<GetBuildsByUserData>(
`/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,
};
}

View File

@ -1 +1,2 @@
export type Unpacked<T> = T extends (infer U)[] ? U : T;
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

View File

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

View File

@ -0,0 +1,20 @@
import { NextApiRequest, NextApiResponse } from "next";
import {
getBuildsByUser,
GetBuildsByUserData,
} from "prisma/queries/getBuildsByUser";
const usersBuildsHandler = async (
req: NextApiRequest,
res: NextApiResponse<GetBuildsByUserData>
) => {
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;

View File

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

View File

@ -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 && (
<ProfileModal onClose={() => setShowModal(false)} user={user} />
)}
<Divider my="2em" />
{user.profile?.bio && (
<Box>
<>
<Divider my={6} />
<Markdown value={user.profile.bio} />
</Box>
</>
)}
{buildCount > 0 && (
<>
<Divider my={6} />
{buildCount > 6 && (
<Select
onChange={(e) =>
setWeapon(e.target.value === "ALL" ? null : e.target.value)
}
mx="auto"
maxWidth={80}
size="lg"
>
<option value="ALL">All weapons ({buildCount})</option>
{weaponCounts.map((wpnTuple) => (
<option value={wpnTuple[0]}>
{wpnTuple[0]} ({wpnTuple[1]})
</option>
))}
</Select>
)}
<MyInfiniteScroller>
{builds.map((build) => (
<BuildCard key={build.id} build={build} m={2} />
))}
</MyInfiniteScroller>
</>
)}
</>
);

View File

@ -0,0 +1,12 @@
import { PromiseReturnType } from "@prisma/client";
import DBClient from "prisma/client";
export type GetBuildsByUserData = PromiseReturnType<typeof getBuildsByUser>;
const prisma = DBClient.getInstance().prisma;
export const getBuildsByUser = async (userId: number) =>
prisma.build.findMany({
where: { userId },
orderBy: [{ updatedAt: "desc" }],
});

View File

@ -20,6 +20,9 @@ export const getUserByIdentifier = (identifier: string) =>
customUrlPath: identifier.toLowerCase(),
},
},
{
id: Number(identifier),
},
],
},
select: {