mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-29 14:50:13 -05:00
closes #194
This commit is contained in:
@@ -7,6 +7,7 @@ interface Props {
|
||||
href: string;
|
||||
isExternal?: boolean;
|
||||
prefetch?: boolean;
|
||||
isColored?: boolean;
|
||||
}
|
||||
|
||||
const MyLink: React.FC<Props> = ({
|
||||
@@ -14,19 +15,22 @@ const MyLink: React.FC<Props> = ({
|
||||
href,
|
||||
isExternal,
|
||||
prefetch = true,
|
||||
isColored = true,
|
||||
}) => {
|
||||
const { themeColorShade } = useMyTheme();
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<ChakraLink href={href} color={themeColorShade}>
|
||||
<ChakraLink href={href} color={isColored ? themeColorShade : undefined}>
|
||||
{children}
|
||||
</ChakraLink>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NextLink href={href} prefetch={prefetch}>
|
||||
<ChakraLink color={themeColorShade}>{children}</ChakraLink>
|
||||
<NextLink href={href} prefetch={prefetch ? undefined : false} passHref>
|
||||
<ChakraLink color={isColored ? themeColorShade : undefined}>
|
||||
{children}
|
||||
</ChakraLink>
|
||||
</NextLink>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ const FooterContent: React.FC = () => {
|
||||
const { themeColorHex: themeColor } = useMyTheme();
|
||||
return (
|
||||
<Flex
|
||||
mt="-1px"
|
||||
pt={4}
|
||||
pb="50px"
|
||||
flexShrink={0}
|
||||
alignItems="center"
|
||||
|
||||
18
lib/useDebounce.ts
Normal file
18
lib/useDebounce.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// https://usehooks.com/useDebounce/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useDebounce(value: string, delay: number = 500) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
144
pages/u/index.tsx
Normal file
144
pages/u/index.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
} from "@chakra-ui/react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import Breadcrumbs from "components/common/Breadcrumbs";
|
||||
import MyLink from "components/common/MyLink";
|
||||
import UserAvatar from "components/common/UserAvatar";
|
||||
import { getEmojiFlag } from "countries-list";
|
||||
import { Unpacked } from "lib/types";
|
||||
import { useDebounce } from "lib/useDebounce";
|
||||
import { useMyTheme } from "lib/useMyTheme";
|
||||
import { GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { getAllUsers, GetAllUsersData } from "prisma/queries/getAllUsers";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FaTwitter } from "react-icons/fa";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
|
||||
interface Props {
|
||||
users: GetAllUsersData;
|
||||
}
|
||||
|
||||
const UserSearchPage = ({ users }: Props) => {
|
||||
const router = useRouter();
|
||||
const { gray } = useMyTheme();
|
||||
|
||||
const [filter, setFilter] = useState(getInitialFilterValue());
|
||||
const debouncedFilter = useDebounce(filter);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedFilter.length < 3) {
|
||||
router.push(`/u`, undefined, { shallow: true });
|
||||
} else {
|
||||
router.push(`/u?filter=${debouncedFilter}`, undefined, { shallow: true });
|
||||
}
|
||||
}, [debouncedFilter]);
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
if (debouncedFilter.length < 3) return [];
|
||||
|
||||
return users.filter((u) => {
|
||||
const normalizedFilter = debouncedFilter.toLowerCase();
|
||||
const normalizedName = `${u.username}#${u.discriminator}`.toLowerCase();
|
||||
|
||||
if (normalizedName.includes(normalizedFilter)) return true;
|
||||
|
||||
const normalizedTwitter = u.profile?.twitterName
|
||||
? u.profile.twitterName.toLowerCase()
|
||||
: "";
|
||||
|
||||
if (normalizedTwitter.includes(normalizedFilter)) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
}, [debouncedFilter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<meta name="robots" content="noindex" />
|
||||
</Head>
|
||||
<Breadcrumbs pages={[{ name: t`Users` }]} />
|
||||
<Box color={gray} fontSize="sm" mb={4}>
|
||||
<Trans>Search for users by their Discord tag or Twitter name</Trans>
|
||||
</Box>
|
||||
<InputGroup size="lg" maxW={80} mt={4} mb={6}>
|
||||
<InputLeftElement
|
||||
pointerEvents="none"
|
||||
children={<Box as={FiSearch} color={gray} />}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Sendou#4059"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Box mt={4}>
|
||||
{filteredUsers.map((user) => (
|
||||
<UserInfo key={user.discordId} user={user} />
|
||||
))}
|
||||
</Box>
|
||||
{debouncedFilter.length >= 3 && !filteredUsers.length && (
|
||||
<Box>No matching users found</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
function getInitialFilterValue() {
|
||||
if (!router.query.filter || typeof router.query.filter !== "string")
|
||||
return "";
|
||||
|
||||
return router.query.filter;
|
||||
}
|
||||
};
|
||||
|
||||
function UserInfo({ user }: { user: Unpacked<GetAllUsersData> }) {
|
||||
return (
|
||||
<Flex my={6} align="center">
|
||||
<Link href={`/u/${user.discordId}`}>
|
||||
<a>
|
||||
<UserAvatar isSmall user={user} />
|
||||
</a>
|
||||
</Link>
|
||||
<Box ml={4}>
|
||||
<Box fontWeight="bold">
|
||||
<MyLink href={`/u/${user.discordId}`} isColored={false}>
|
||||
{user.username}#{user.discriminator}
|
||||
</MyLink>
|
||||
{user.profile?.country ? (
|
||||
<Box as="span" ml={2}>
|
||||
{getEmojiFlag(user.profile.country)}
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
{user.profile?.twitterName && (
|
||||
<Flex align="center" fontSize="sm" mt={1}>
|
||||
<Box as={FaTwitter} color="#00acee" mr={1} />
|
||||
<MyLink
|
||||
isExternal
|
||||
href={`https://twitter.com/${user.profile.twitterName}`}
|
||||
isColored={false}
|
||||
>
|
||||
{user.profile.twitterName}
|
||||
</MyLink>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props> = async () => {
|
||||
await getAllUsers();
|
||||
|
||||
return { props: { users: await getAllUsers() }, revalidate: 3600 };
|
||||
};
|
||||
|
||||
export default UserSearchPage;
|
||||
20
prisma/queries/getAllUsers.ts
Normal file
20
prisma/queries/getAllUsers.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import prisma from "prisma/client";
|
||||
|
||||
export type GetAllUsersData = Prisma.PromiseReturnType<typeof getAllUsers>;
|
||||
|
||||
export const getAllUsers = async () =>
|
||||
prisma.user.findMany({
|
||||
select: {
|
||||
discordId: true,
|
||||
username: true,
|
||||
discriminator: true,
|
||||
discordAvatar: true,
|
||||
profile: {
|
||||
select: {
|
||||
country: true,
|
||||
twitterName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user