- add imprint
All checks were successful
continuous-integration/drone/push Build is passing

- add persistence
This commit is contained in:
2022-05-16 02:11:45 +02:00
parent bb96475833
commit 71fa0133e3
16 changed files with 928 additions and 111 deletions

View File

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

View File

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

View File

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

View File

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

View 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;

View File

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

View File

@@ -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&apos;s Okay!</Text>
<Button onClick={handleBackToHome}>Let&apos;s Head Back</Button>
</Box>
<Center flexDirection="column">
<Button onClick={handleBackToHome}>Go back home</Button>
</Center>
</Grid>
);
};

View 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;

View File

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

View File

@@ -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",
},
}),
},
},

View File

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

View 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,
};
}

View 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,
};
}