- add persistence
This commit is contained in:
24
src/App.tsx
24
src/App.tsx
@@ -13,17 +13,25 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
import Layout from "Components/Layout";
|
||||
import Page404 from "Sites/404";
|
||||
import { routes } from "Sites";
|
||||
import { SWRConfig } from "swr";
|
||||
|
||||
const App = () => (
|
||||
<Router>
|
||||
<Layout>
|
||||
<Routes>
|
||||
{routes.map((routeProps) => (
|
||||
<Route {...routeProps} key={routeProps.path as string} />
|
||||
))}
|
||||
<Route path="*" element={<Page404 />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: (resource, init) =>
|
||||
fetch(resource, init).then((res) => res.json()),
|
||||
}}
|
||||
>
|
||||
<Layout>
|
||||
<Routes>
|
||||
{routes.map((routeProps) => (
|
||||
<Route {...routeProps} key={routeProps.path as string} />
|
||||
))}
|
||||
<Route path="*" element={<Page404 />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</SWRConfig>
|
||||
</Router>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box, Text, Heading, useStyleConfig, chakra } from "@chakra-ui/react";
|
||||
import { ease } from "@unom/style";
|
||||
import { motion, isValidMotionProp } from "framer-motion";
|
||||
import { VFC } from "react";
|
||||
|
||||
@@ -15,9 +16,14 @@ const CardTeam: VFC<CardTeamProps> = ({ players, index }) => {
|
||||
const styles = useStyleConfig("Card", { variant: "smooth" });
|
||||
return (
|
||||
<Container
|
||||
animate="enter"
|
||||
initial="from"
|
||||
variants={{ enter: { scale: 1 }, from: { scale: 0 } }}
|
||||
variants={{
|
||||
enter: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: ease.quint(0.8).out,
|
||||
},
|
||||
from: { y: 100, opacity: 0 },
|
||||
}}
|
||||
__css={styles}
|
||||
>
|
||||
<Heading size="md" as="h3">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flex, Link, Text } from "@chakra-ui/react";
|
||||
import { Link as LinkComponent } from "react-router-dom";
|
||||
import { Flex, Link, Stack, Text } from "@chakra-ui/react";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
@@ -8,7 +9,13 @@ const Footer = () => {
|
||||
align="center"
|
||||
alignSelf="flex-end"
|
||||
justifyContent="center"
|
||||
></Flex>
|
||||
>
|
||||
<Stack>
|
||||
<Link to="/legal/imprint" as={LinkComponent}>
|
||||
Imprint
|
||||
</Link>
|
||||
</Stack>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<Box margin="0 auto" maxWidth={800} transition="0.5s ease-out">
|
||||
<Meta />
|
||||
<Flex wrap="wrap" margin="8" minHeight="90vh">
|
||||
<Flex wrap="wrap" margin="8" minHeight="100vh">
|
||||
<Header />
|
||||
<ContentContainer
|
||||
transition={{ type: "tween", ease: "circOut" }}
|
||||
|
||||
70
src/Sections/TeamGenerator/Input.tsx
Normal file
70
src/Sections/TeamGenerator/Input.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { Button, Input, InputGroup, InputRightElement } from "@chakra-ui/react";
|
||||
import { ease } from "@unom/style";
|
||||
import { motion } from "framer-motion";
|
||||
import { VFC } from "react";
|
||||
import type { UseFormGetValues, UseFormRegister } from "react-hook-form";
|
||||
|
||||
const PlayerNameInput: VFC<{
|
||||
index: number;
|
||||
onRemove: () => void;
|
||||
onAppend: () => void;
|
||||
fieldsLength: number;
|
||||
getValues: UseFormGetValues<any>;
|
||||
register: UseFormRegister<any>;
|
||||
}> = ({ index, onRemove, onAppend, register, fieldsLength, getValues }) => {
|
||||
return (
|
||||
<InputGroup
|
||||
as={motion.div}
|
||||
exit="exit"
|
||||
animate="enter"
|
||||
initial="from"
|
||||
variants={{
|
||||
enter: {
|
||||
scale: 1,
|
||||
transition: ease.quint(0.8).out,
|
||||
},
|
||||
from: {
|
||||
scale: 0,
|
||||
},
|
||||
exit: {
|
||||
scale: 0,
|
||||
transition: {
|
||||
type: "tween",
|
||||
ease: "circIn",
|
||||
duration: 0.2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
transition="none"
|
||||
>
|
||||
{index !== 0 && (
|
||||
<InputRightElement>
|
||||
<Button mr="0.25rem" size="sm" onClick={() => onRemove()}>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
)}
|
||||
<Input
|
||||
placeholder={`Player ${index + 1} name`}
|
||||
size="md"
|
||||
type="name"
|
||||
{...register(`players.${index}.value`)}
|
||||
autoFocus={index === fieldsLength}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && index === fieldsLength - 1) {
|
||||
e.preventDefault();
|
||||
const value = getValues(`players.${index}.value`);
|
||||
if (value !== "") {
|
||||
onAppend();
|
||||
} else if (index !== 0) {
|
||||
onRemove();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayerNameInput;
|
||||
@@ -1,28 +1,21 @@
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Text,
|
||||
Stack,
|
||||
SimpleGrid,
|
||||
chakra,
|
||||
Input,
|
||||
InputRightElement,
|
||||
InputGroup,
|
||||
Switch,
|
||||
} from "@chakra-ui/react";
|
||||
import CardTeam from "Components/Cards/Team";
|
||||
import { AnimatePresence, isValidMotionProp, motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import useTeamGenerator from "lib/Hooks/useTeamGenerator";
|
||||
import { useEffect } from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
|
||||
const getRandom = (list: Array<any>) => {
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
};
|
||||
import PlayerNameInput from "./Input";
|
||||
|
||||
const SectionTeamGenerator = () => {
|
||||
const [teams, setTeams] = useState<Array<Array<string>>>([]);
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
@@ -36,22 +29,23 @@ const SectionTeamGenerator = () => {
|
||||
name: "players",
|
||||
});
|
||||
|
||||
const { teams, players, setConfig, setPlayers, generate, set, save } =
|
||||
useTeamGenerator();
|
||||
|
||||
useEffect(() => {
|
||||
if (players.length > 0) {
|
||||
remove(0);
|
||||
players.map((name) => append({ value: name }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<form
|
||||
onSubmit={handleSubmit((values) => {
|
||||
const shuffledPlayers = [...values.players]
|
||||
.sort(() => (Math.random() > 0.5 ? 1 : -1))
|
||||
.map((value) => value.value);
|
||||
|
||||
const teams = [];
|
||||
|
||||
const range = Math.ceil(values.players.length / values.teamCount);
|
||||
while (shuffledPlayers.length > 0) {
|
||||
teams.push(shuffledPlayers.splice(0, range));
|
||||
}
|
||||
|
||||
setTeams(teams);
|
||||
setConfig({ teamCount: values.teamCount });
|
||||
setPlayers(values.players.map((player) => player.value));
|
||||
generate();
|
||||
})}
|
||||
>
|
||||
<Stack mb={6} spacing={3}>
|
||||
@@ -60,65 +54,22 @@ const SectionTeamGenerator = () => {
|
||||
<Stack spacing={3}>
|
||||
<AnimatePresence>
|
||||
{fields.map((field, index) => (
|
||||
<InputGroup
|
||||
as={motion.div}
|
||||
animate={{
|
||||
scale: 1,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
damping: 15,
|
||||
mass: 1,
|
||||
},
|
||||
}}
|
||||
transition="none"
|
||||
initial={{ scale: 0 }}
|
||||
exit={{
|
||||
scale: 0,
|
||||
transition: {
|
||||
type: "tween",
|
||||
ease: "circIn",
|
||||
duration: 0.2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{index !== 0 && (
|
||||
<InputRightElement>
|
||||
<Button
|
||||
mr="0.25rem"
|
||||
size="sm"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
)}
|
||||
<Input
|
||||
placeholder={`Player ${index + 1} name`}
|
||||
key={field.id}
|
||||
size="md"
|
||||
type="name"
|
||||
{...register(`players.${index}.value`)}
|
||||
autoFocus={index === fields.length}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && index === fields.length - 1) {
|
||||
e.preventDefault();
|
||||
const value = getValues(`players.${index}.value`);
|
||||
if (value !== "") {
|
||||
append({ value: "" });
|
||||
} else {
|
||||
remove(index);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
<PlayerNameInput
|
||||
register={register}
|
||||
fieldsLength={fields.length}
|
||||
getValues={getValues}
|
||||
index={index}
|
||||
onAppend={() => append({ value: "" })}
|
||||
onRemove={() => remove(index)}
|
||||
key={field.id}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</Stack>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<Stack spacing={6} as={motion.div} layout>
|
||||
|
||||
<Stack spacing={6}>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="team-count">Team count</FormLabel>
|
||||
<Input
|
||||
@@ -129,15 +80,37 @@ const SectionTeamGenerator = () => {
|
||||
defaultValue={2}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="save-players">Save players?</FormLabel>
|
||||
<Switch
|
||||
defaultChecked={save}
|
||||
onChange={(e) => {
|
||||
set({ save: e.target.checked });
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button type="submit">Generate</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
<SimpleGrid as={motion.div} columns={2} mt="1rem" spacing="1.5rem">
|
||||
{teams.map((team, index) => (
|
||||
<CardTeam players={team} index={index} key={index} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{teams.length > 0 && (
|
||||
<SimpleGrid
|
||||
variants={{
|
||||
enter: { transition: { staggerChildren: 0.2 } },
|
||||
from: {},
|
||||
}}
|
||||
as={motion.div}
|
||||
columns={2}
|
||||
animate="enter"
|
||||
initial="from"
|
||||
mt="2.5rem"
|
||||
spacing="1.5rem"
|
||||
>
|
||||
{teams.map((team, index) => (
|
||||
<CardTeam players={team} index={index} key={index} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Grid,
|
||||
Heading,
|
||||
Image,
|
||||
Link,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Page404 = () => {
|
||||
@@ -19,16 +21,19 @@ const Page404 = () => {
|
||||
<Heading>Page not Found</Heading>
|
||||
|
||||
<Box maxWidth={[280, 400]} marginX="auto">
|
||||
<Image width={400} src="/assets/404 Error-rafiki.svg" />
|
||||
<Link fontSize="xs" href="https://stories.freepik.com/web" isExternal>
|
||||
Illustration by Freepik Stories
|
||||
</Link>
|
||||
<motion.h1
|
||||
transition={{ type: "tween", ease: "circOut", duration: 1 }}
|
||||
animate={{ scale: 1 }}
|
||||
initial={{ scale: 0 }}
|
||||
style={{ fontSize: "8rem" }}
|
||||
>
|
||||
404
|
||||
</motion.h1>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text>It's Okay!</Text>
|
||||
<Button onClick={handleBackToHome}>Let's Head Back</Button>
|
||||
</Box>
|
||||
<Center flexDirection="column">
|
||||
<Button onClick={handleBackToHome}>Go back home</Button>
|
||||
</Center>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
23
src/Sites/Legal/imprint.tsx
Normal file
23
src/Sites/Legal/imprint.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import ChakraUIRenderer from "chakra-ui-markdown-renderer";
|
||||
import useImprint from "lib/Hooks/useImprint";
|
||||
import { lazy } from "react";
|
||||
import { theme } from "Styles/customTheme";
|
||||
|
||||
const ReactMarkdown = lazy(() => import("react-markdown"));
|
||||
|
||||
const SiteImprint = () => {
|
||||
const { content, error } = useImprint();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ReactMarkdown
|
||||
components={ChakraUIRenderer(theme)}
|
||||
children={content}
|
||||
skipHtml
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteImprint;
|
||||
@@ -1,11 +1,21 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { PathRouteProps } from "react-router-dom";
|
||||
|
||||
import Start from "Sites/Start";
|
||||
import SiteStart from "Sites/Start";
|
||||
import SiteImprint from "./Legal/imprint";
|
||||
|
||||
export const routes: Array<PathRouteProps> = [
|
||||
{
|
||||
path: "/",
|
||||
element: <Start />,
|
||||
element: <SiteStart />,
|
||||
},
|
||||
{
|
||||
path: "/legal/imprint",
|
||||
element: (
|
||||
<Suspense fallback={<></>}>
|
||||
<SiteImprint />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -140,9 +140,28 @@ export const theme = extendTheme(
|
||||
background: props.colorMode === "dark" ? "black" : "white",
|
||||
lineHeight: "tall",
|
||||
},
|
||||
"h1.chakra-heading": {
|
||||
fontSize: "var(--chakra-fontSizes-5xl)",
|
||||
},
|
||||
"h2.chakra-heading": {
|
||||
fontSize: "var(--chakra-fontSizes-3xl)",
|
||||
},
|
||||
"h3.chakra-heading": {
|
||||
fontSize: "var(--chakra-fontSizes-2xl)",
|
||||
},
|
||||
"h4.chakra-heading": {
|
||||
fontSize: "var(--chakra-fontSizes-lg)",
|
||||
},
|
||||
"h5.chakra-heading": {
|
||||
fontSize: "var(--chakra-fontSizes-md)",
|
||||
},
|
||||
a: {
|
||||
color: props.colorMode === "dark" ? "teal.300" : "teal.500",
|
||||
},
|
||||
p: {
|
||||
whiteSpace: "pre-line",
|
||||
maxWidth: "700px",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChakraProvider } from "@chakra-ui/react";
|
||||
import { ChakraProvider, ColorModeScript } from "@chakra-ui/react";
|
||||
import { StrictMode } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
@@ -11,6 +11,7 @@ import { theme } from "Styles/customTheme";
|
||||
|
||||
ReactDOM.render(
|
||||
<StrictMode>
|
||||
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||
<ChakraProvider theme={theme}>
|
||||
<App />
|
||||
</ChakraProvider>
|
||||
|
||||
15
src/lib/Hooks/useImprint.ts
Normal file
15
src/lib/Hooks/useImprint.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function useImprint() {
|
||||
const { data, error } = useSWR(
|
||||
"https://api.enrico.buehler.earth/api/imprint"
|
||||
);
|
||||
|
||||
const content = data && data.data.attributes?.content;
|
||||
|
||||
return {
|
||||
data,
|
||||
content,
|
||||
error,
|
||||
};
|
||||
}
|
||||
76
src/lib/Hooks/useTeamGenerator.ts
Normal file
76
src/lib/Hooks/useTeamGenerator.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useCallback } from "react";
|
||||
import create from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
type TeamGeneratorStore = {
|
||||
players: Array<string>;
|
||||
set: (input: Omit<Partial<TeamGeneratorStore>, "set">) => void;
|
||||
get: () => TeamGeneratorStore;
|
||||
setConfig: (config: Config) => void;
|
||||
save: boolean;
|
||||
setPlayers: (players: Array<string>) => void;
|
||||
teams: Array<Array<string>>;
|
||||
config: Config;
|
||||
};
|
||||
|
||||
type Config = {
|
||||
teamCount: number;
|
||||
};
|
||||
|
||||
const useTeamGeneratorStore = create(
|
||||
persist<TeamGeneratorStore>(
|
||||
(set, get) => ({
|
||||
set,
|
||||
save: false,
|
||||
get,
|
||||
setPlayers: (players) => set({ players }),
|
||||
setConfig: (config) => set({ config }),
|
||||
players: [],
|
||||
teams: [],
|
||||
config: {
|
||||
teamCount: 2,
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "team-generator-storage",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export { useTeamGeneratorStore };
|
||||
|
||||
export default function useTeamGenerator() {
|
||||
const { players, teams, set, config, setConfig, setPlayers, get, save } =
|
||||
useTeamGeneratorStore();
|
||||
|
||||
const generate = () => {
|
||||
const store = get();
|
||||
|
||||
const shuffledPlayers = [...store.players].sort(() =>
|
||||
Math.random() > 0.5 ? 1 : -1
|
||||
);
|
||||
|
||||
const _teams = [];
|
||||
|
||||
const range = Math.ceil(store.players.length / config.teamCount);
|
||||
while (shuffledPlayers.length > 0) {
|
||||
_teams.push(shuffledPlayers.splice(0, range));
|
||||
}
|
||||
|
||||
set({ teams: _teams });
|
||||
|
||||
if (!store.save) {
|
||||
localStorage.removeItem("team-generator-storage");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
generate,
|
||||
setConfig,
|
||||
set,
|
||||
save,
|
||||
setPlayers,
|
||||
players,
|
||||
teams,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user