mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-16 08:27:05 -05:00
builds on profile page
This commit is contained in:
parent
8a10eb265d
commit
03af9fa7bb
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
29
hooks/u.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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>>;
|
||||
|
|
|
|||
|
|
@ -1,21 +1,17 @@
|
|||
[](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
|
||||
|
|
|
|||
20
pages/api/users/[id]/builds.ts
Normal file
20
pages/api/users/[id]/builds.ts
Normal 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;
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
12
prisma/queries/getBuildsByUser.ts
Normal file
12
prisma/queries/getBuildsByUser.ts
Normal 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" }],
|
||||
});
|
||||
|
|
@ -20,6 +20,9 @@ export const getUserByIdentifier = (identifier: string) =>
|
|||
customUrlPath: identifier.toLowerCase(),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Number(identifier),
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user