First release
This commit is contained in:
parent
829456339e
commit
c87a897f7f
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
|
@ -0,0 +1,152 @@
|
|||
{
|
||||
"characters": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Gawr Gura",
|
||||
"image": "https://characterai.io/i/400/static/avatars/oL2IzOD15_wBIP_o6NAWDwiVyAnzz_3aGLu9aU7i254/PvvL1eslO5go1byUXhH-gLRw_HCkFc7KpW_i6ojDPfQ.webp",
|
||||
"creator": "stinkychum",
|
||||
"description": "Shark-girl Idol of Hololive EN !"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Rushia Uruha",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/6/TwLdQ0F1qfRlxY4HBiOBG51onUZG2cuvtVAcPhd33lE.webp",
|
||||
"creator": "maladroit",
|
||||
"description": "your cute yandere girlfriend"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Mori Calliope",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/qJ6Y0Awr0r0IDfP_T0UVdkjgZGhePCtp5gdbXN4jT8k.webp",
|
||||
"creator": "trun25554",
|
||||
"description": "I am Mori Calliope, English Virtual YouTuber, part of Hololive EN~"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Amelia Watson",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/D24yxjMB0YTH0YBaPeBkaZcDwU8JSZjfBFDZWIILlKI.webp",
|
||||
"creator": "trun25554",
|
||||
"description": "Heeelloo, it's Amelia Watson, English Virtual YouTuber, part of Hololive EN 1st Generation~"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Pipkin Pippa",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/6/BtUEqYZYEweIhWhWDbISA_2TlSIL6S24XejGMEmp1eo.webp",
|
||||
"creator": "mundane",
|
||||
"description": "PIPIPIPI!!! I'm the cutest rabbit in the universe!"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Hoshimachi Suisei",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/S564dpkpF-HFN9vu5iOs4LjkQGtIm4KJKSWfrkRJrzg.webp",
|
||||
"creator": "MahoHiyajo",
|
||||
"description": "I'm your idol VTuber Hoshimachi Suisei!"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Shirakami Fubuki",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/6/8wd3Cm4I5zVYjahtPopdIcItDD3f-EgB75MgFKOqhvg.webp",
|
||||
"creator": "MahoHiyajo",
|
||||
"description": "I am Shirakami Fubuki, virtual fox of Hololive!"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Laplus Darkness",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/7/rPeXKHHJ1LD6oKlcNaLv4gEYZd4pIABU3AS9EJgyeKg.webp",
|
||||
"creator": "Farenna",
|
||||
"description": "The Leader of HoloX from Hololive Gen 6!"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Nanashi Mumei",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/5/eo9KFPGEZb6vR3PsrXTW1bf4scYVY5xyZ3jKpwUC28I.webp",
|
||||
"creator": "MahoHiyajo",
|
||||
"description": "Oh hi! I am Nanashi Mumei from Hololive English!"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Nyatasha Nyanners",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/z4_5IKIeFCvrTKxPxtgGJYTngjdbJP4mcwGBZZB4hMI.webp",
|
||||
"creator": "makifoxgirl",
|
||||
"description": "im a vtuber and singer"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "Takanashi Kiara",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/vSjzwmn-COUNjv7AF4CWKzOu3imxTRvrj5_mGbU4J68.webp",
|
||||
"creator": "Sarve",
|
||||
"description": "Vtuber, Idol, part-time warrior, am Phoenix!"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Ninomae Inanis",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/27/jxY2eF0UqiM0uAobpr5753cNjSinuPB0p8kUG2qUeSY.webp",
|
||||
"creator": "AndyYuno",
|
||||
"description": "Wah! I am Ninomae Ina'nis, a Virtual Youtuber from Hololive EN!"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "Usada Pekora",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/5/on9CzLR9Cp78Wl_W3zsEe0ldMWfhq-QY6BZG-Zmriwg.webp",
|
||||
"creator": "MahoHiyajo",
|
||||
"description": "Konpeko! Konpeko! Konpeko! I'm Usada Pekora-peko!"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "Minato Aqua",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/6/wC5j5y3sYr9Y8I7AE15VOFVmoT3Qrh_1WzVheyIXwLY.webp",
|
||||
"creator": "Rairu",
|
||||
"description": "Hololive idol gamer maid"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "Inugami Korone",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/6/vZMxHfgpzkckaN9s8aiCSFeX09ZhRBnnS-vnq4uDeow.webp",
|
||||
"creator": "MahoHiyajo",
|
||||
"description": "I'm Inugami Korone from Hololive! Yubi! Yubi!"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Murasaki Shion",
|
||||
"image": "https://characterai.io/i/400/static/avatars/uploaded/2022/10/7/Ox2fpcJ1tn_Qydw_DmZwjzyAZXuW5FKnRU0fgxy2_Ho.webp",
|
||||
"creator": "MahoHiyajo",
|
||||
"description": "A young magician from Hololive, Murasaki Shion!"
|
||||
}
|
||||
],
|
||||
"chats": [
|
||||
{
|
||||
"id": 1,
|
||||
"messages": [
|
||||
{
|
||||
"text": "This is an example message from Gura. It's the first message sent.",
|
||||
"sender": "CHARACTER"
|
||||
},
|
||||
{
|
||||
"text": "This is a message you sent back to Gura.",
|
||||
"sender": "YOU"
|
||||
},
|
||||
{
|
||||
"text": "This is Gura's response to your message. Hello!",
|
||||
"sender": "CHARACTER"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"messages": [
|
||||
{
|
||||
"text": "Here's another example message, this time from Rushia.",
|
||||
"sender": "CHARACTER"
|
||||
},
|
||||
{
|
||||
"text": "This is your response.",
|
||||
"sender": "YOU"
|
||||
},
|
||||
{
|
||||
"text": "This is Rushia's 2nd message.",
|
||||
"sender": "CHARACTER"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pygmalion Web Frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
function useColor(light="white", dark="#1A202C"){
|
||||
// Fixes the light theme flashing issue when loading a page. Assumes that the theme starts in dark mode.
|
||||
const colorMode = window.localStorage.getItem("chakra-ui-color-mode");
|
||||
if(colorMode === null){
|
||||
return dark;
|
||||
}
|
||||
return colorMode === "dark" ? dark : light;
|
||||
}
|
||||
document.body.style.background = useColor();
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "pygmalion-web-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.14",
|
||||
"@chakra-ui/react": "^2.4.4",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"framer-motion": "^6.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,45 @@
|
|||
const SERVER_URL = "http://localhost:3000"; // Based on json-server's defaults
|
||||
|
||||
export async function getCharacters(query){
|
||||
const url = SERVER_URL + "/characters";
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if(!response.ok){
|
||||
console.log(response.status, response.statusText);
|
||||
return {};
|
||||
}
|
||||
else{
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCharacter(charId){
|
||||
const url = SERVER_URL + "/characters/" + charId;
|
||||
const response = await fetch(url);
|
||||
if(!response.ok){
|
||||
console.log(response.status, response.statusText);
|
||||
return {};
|
||||
}
|
||||
else{
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getChats(charId){
|
||||
const url = SERVER_URL + "/chats/" + charId;
|
||||
const response = await fetch(url);
|
||||
if(!response.ok){
|
||||
console.log(response.status, response.statusText);
|
||||
return {};
|
||||
}
|
||||
else{
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,30 @@
|
|||
import { Card, CardBody, Image, Stack, Heading, Text, AspectRatio, LinkBox, LinkOverlay } from "@chakra-ui/react";
|
||||
|
||||
const CharacterCard = (props) => {
|
||||
return (
|
||||
<Card maxWidth="sm" h="100%">
|
||||
<CardBody p="0">
|
||||
<LinkBox>
|
||||
<LinkOverlay href={"/chat/" + props.charId}>
|
||||
<AspectRatio maxWidth="100%" ratio={1}>
|
||||
<Image
|
||||
src={props.image}
|
||||
alt={props.alt}
|
||||
borderRadius="8px 8px 0 0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</AspectRatio>
|
||||
</LinkOverlay>
|
||||
</LinkBox>
|
||||
<Stack mt="6" spacing="3" m="0.5rem" maxHeight="9rem">
|
||||
<Heading size="md" className="line-clamp" overflow="hidden" textOverflow="ellipsis">{props.name}</Heading>
|
||||
<Text fontStyle="italic" fontSize="0.75rem">@{props.creator}</Text>
|
||||
<Text fontSize="0.75rem" overflow="hidden" textOverflow="ellipsis" className="line-clamp-2">{props.description}</Text>
|
||||
</Stack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CharacterCard;
|
|
@ -0,0 +1,138 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
Link, Box, Flex, Button, Image, Stack, IconButton,
|
||||
useColorMode, useColorModeValue
|
||||
} from "@chakra-ui/react";
|
||||
import { SunIcon, MoonIcon, HamburgerIcon, CloseIcon, SearchIcon, AddIcon } from "@chakra-ui/icons";
|
||||
|
||||
import logo from "../assets/wAIfu_20flag_20square.png";
|
||||
|
||||
// Navbar component code based on https://codesandbox.io/s/b22b7
|
||||
|
||||
const NavBar = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggle = () => setIsOpen(!isOpen);
|
||||
|
||||
const logoBorderColor = useColorModeValue("var(--flag-color-dark-blue)", "var(--chakra-colors-whiteAlpha-800)");
|
||||
|
||||
return (
|
||||
<NavBarContainer boxShadow="base" p="1rem">
|
||||
<Link href="/" boxSize="min(48px, 100%)">
|
||||
<Image
|
||||
borderRadius="full"
|
||||
height="100%"
|
||||
border="2px"
|
||||
borderColor={logoBorderColor}
|
||||
src={logo}
|
||||
alt="wAIfu flag logo"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Stack direction="row">
|
||||
<Box display={{ base: "block", md: "none"}}>
|
||||
<ColorThemeSwitch />
|
||||
</Box>
|
||||
<NavToggle toggle={toggle} isOpen={isOpen} />
|
||||
</Stack>
|
||||
<NavMenu isOpen={isOpen} />
|
||||
</NavBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const NavToggle = ({ toggle, isOpen }) => {
|
||||
return (
|
||||
<IconButton
|
||||
display={{ base: "block", md: "none" }}
|
||||
height="auto"
|
||||
colorScheme="gray"
|
||||
color="gray.400"
|
||||
bg="#ffffff00"
|
||||
size="lg"
|
||||
aria-label="Navigation menu toggle"
|
||||
icon={isOpen ? <CloseIcon height="100%" /> : <HamburgerIcon height="100%" />}
|
||||
onClick={toggle}>
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
const NavItem = ({ children, to = "/", ...rest}) => {
|
||||
return (
|
||||
<Link href={to} {...rest}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const NavMenu = ({ isOpen }) => {
|
||||
return (
|
||||
<Box
|
||||
display={{ base: isOpen ? "block" : "none", md: "block" }}
|
||||
flexBasis={{ base: "100%", md: "auto" }}
|
||||
height="min(48px, 100%)"
|
||||
>
|
||||
<Stack
|
||||
spacing={8}
|
||||
align="center"
|
||||
justify={["center", "center", "flex-end", "flex-end"]}
|
||||
direction={["column", "row", "row", "row"]}
|
||||
pt={[4, 4, 0, 0]}
|
||||
height="100%"
|
||||
>
|
||||
<NavItem to="/search">
|
||||
<Button as={SearchIcon} bg="#ffffff00" boxSize="3.12rem" />
|
||||
</NavItem>
|
||||
<NavItem to="/create">
|
||||
<Button as={AddIcon} bg="#ffffff00" boxSize="3.12rem" />
|
||||
</NavItem>
|
||||
<NavItem to="/about">About</NavItem>
|
||||
<Box display={{ base: "none", md: "block"}}>
|
||||
<ColorThemeSwitch />
|
||||
</Box>
|
||||
<NavItem to="/signup">
|
||||
<Button size="sm" rounded="md">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavItem>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const NavBarContainer = ({ children, ...props }) => {
|
||||
return (
|
||||
<Flex
|
||||
as="nav"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
wrap="wrap"
|
||||
width="100%"
|
||||
mb={8}
|
||||
p={8}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const ColorThemeSwitch = ({ ...props }) => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
return (
|
||||
<IconButton
|
||||
colorScheme="gray"
|
||||
color="gray.400"
|
||||
bg="#ffffff00"
|
||||
size="lg"
|
||||
aria-label="Switch color themes"
|
||||
icon={colorMode === "dark" ? <SunIcon /> : <MoonIcon />}
|
||||
onClick={() => {
|
||||
document.body.style.background = "";
|
||||
toggleColorMode();
|
||||
}}
|
||||
{...props}>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavBar;
|
|
@ -0,0 +1,30 @@
|
|||
import React from "react";
|
||||
import { Flex, Input, Button } from "@chakra-ui/react";
|
||||
|
||||
const MessageInput = ({ inputMessage, setInputMessage, handleSendMessage }) => {
|
||||
return (
|
||||
<Flex w="100%" mt="5" mb="5">
|
||||
<Input
|
||||
mr="2"
|
||||
placeholder="Type a message"
|
||||
border="2px solid"
|
||||
borderRadius="24px"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
disabled={inputMessage.trim().length <= 0}
|
||||
onClick={handleSendMessage}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageInput;
|
|
@ -0,0 +1,39 @@
|
|||
import React, { useEffect, useRef } from "react";
|
||||
import { Avatar, Flex, Text } from "@chakra-ui/react";
|
||||
|
||||
const Messages = ({ messages, character }) => {
|
||||
const AlwaysScrollToBottom = () => {
|
||||
const elementRef = useRef();
|
||||
useEffect(() => elementRef.current.scrollIntoView());
|
||||
return <div ref={elementRef} />;
|
||||
};
|
||||
|
||||
return ( // To make the messages start at the bottom, use flex-direction, not flex-end
|
||||
<Flex width="100%" height="100%" overflowY="scroll" flexDirection="column-reverse" p="3">
|
||||
<AlwaysScrollToBottom />
|
||||
{messages.map((item, index) => {
|
||||
const senderName = item.sender === "CHARACTER" ? character.name : "Anon";
|
||||
const senderImage = item.sender === "CHARACTER" ? character.image : "";
|
||||
return (
|
||||
<Flex key={index} width="100%" mb="4" mt="4">
|
||||
<Avatar
|
||||
name={senderName}
|
||||
src={senderImage}
|
||||
/>
|
||||
<Flex
|
||||
minWidth="100px"
|
||||
maxWidth="100%"
|
||||
ml="3"
|
||||
flexDirection="column"
|
||||
>
|
||||
<Text fontWeight="bold">{senderName}</Text>
|
||||
<Text>{item.text}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Messages;
|
|
@ -0,0 +1,22 @@
|
|||
import { useRouteError } from "react-router-dom";
|
||||
import {
|
||||
Heading,
|
||||
Text,
|
||||
Link,
|
||||
VStack
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
const ErrorPage = () => {
|
||||
const error = useRouteError();
|
||||
console.error(error);
|
||||
|
||||
return (
|
||||
<VStack id="error-page" spacing="24px" mt="4rem">
|
||||
<Heading as="h1" size="4xl">{error.status} - {error.statusText || error.message}</Heading>
|
||||
<Text fontSize="2xl">Sorry, something went wrong.</Text>
|
||||
<Link href="/">Go Back</Link>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorPage;
|
|
@ -0,0 +1,17 @@
|
|||
:root {
|
||||
--flag-color-dark-blue: #002052;
|
||||
--flag-color-dark-magenta: #8F008E;
|
||||
--flag-color-mid-blue: #005282;
|
||||
}
|
||||
|
||||
.line-clamp {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
|
||||
import {
|
||||
ChakraProvider,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
import theme from "./themes/defaultTheme";
|
||||
|
||||
import Root, { loader as rootLoader } from "./routes/root";
|
||||
import ErrorPage from "./error-page";
|
||||
import About from "./routes/about";
|
||||
import Search from "./routes/search";
|
||||
import Create from "./routes/create";
|
||||
import Chat, {
|
||||
loader as characterLoader,
|
||||
} from "./routes/chat";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Root />,
|
||||
errorElement: <ErrorPage />,
|
||||
loader: rootLoader
|
||||
},
|
||||
{
|
||||
path: "/search",
|
||||
element: <Search />,
|
||||
errorElement: <ErrorPage />
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
element: <About />,
|
||||
errorElement: <ErrorPage />
|
||||
},
|
||||
{
|
||||
path: "/create",
|
||||
element: <Create />,
|
||||
errorElement: <ErrorPage />
|
||||
},
|
||||
{
|
||||
path: "/chat/:char",
|
||||
element: <Chat />,
|
||||
errorElement: <ErrorPage />,
|
||||
loader: characterLoader,
|
||||
}
|
||||
]);
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
ReactDOM.createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<ChakraProvider theme={theme}>
|
||||
<RouterProvider router={router} />
|
||||
</ChakraProvider>
|
||||
</React.StrictMode>
|
||||
);
|
|
@ -0,0 +1,12 @@
|
|||
import NavBar from "../components/NavBar";
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<p>TODO About page</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
|
@ -0,0 +1,79 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useLoaderData } from "react-router-dom";
|
||||
import { getCharacter, getChats } from "../api/characters";
|
||||
import { Flex, Divider, Text, Box } from "@chakra-ui/react";
|
||||
import NavBar from "../components/NavBar";
|
||||
import Messages from "../components/chat/Messages";
|
||||
import MessageInput from "../components/chat/MessageInput";
|
||||
|
||||
export async function loader({ params }){
|
||||
return getCharacter(params.char);
|
||||
}
|
||||
|
||||
// Followed https://ordinarycoders.com/blog/article/react-chakra-ui for most of this page.
|
||||
|
||||
const Chat = () => {
|
||||
const character = useLoaderData();
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [inputMessage, setInputMessage] = useState("");
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!inputMessage.trim().length) {
|
||||
return;
|
||||
}
|
||||
const data = inputMessage;
|
||||
|
||||
setMessages((old) => [{ sender: "YOU", text: data }, ...old]);
|
||||
setInputMessage("");
|
||||
|
||||
setMessages((old) => [{ sender: "CHARACTER", text: "This is a placeholder response to your message.", }, ...old]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
getChats(character.id)
|
||||
.then(
|
||||
(response) => {
|
||||
if(mounted){
|
||||
if(response.messages){
|
||||
let messagesList = response.messages.reverse(); // Reversed because the messages section uses flex-direction="column-reverse"
|
||||
setMessages(messagesList);
|
||||
}
|
||||
else{
|
||||
// If no messages found, default to the description.
|
||||
setMessages([{ sender: "CHARACTER", text: character.description }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setMessages([{ sender: "", text: "An error occurred when fetching messages."}]); // TODO this isn't the best way to show an error.
|
||||
});
|
||||
return () => mounted = false;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
height="100vh"
|
||||
>
|
||||
<NavBar />
|
||||
{messages ? (
|
||||
<Flex flexDir="column" height="100%" maxHeight="86vh">
|
||||
<Flex width="100%" maxHeight="100%" justify="center" align="center" flexGrow="1">
|
||||
<Flex width="60%" height="100%" maxHeight="100%" flexDir="column">
|
||||
<Divider />
|
||||
<Messages messages={messages} character={character} />
|
||||
<Divider />
|
||||
<MessageInput inputMessage={inputMessage} setInputMessage={setInputMessage} handleSendMessage={handleSendMessage} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : (
|
||||
<Text ml="4">An error occured when trying to fetch messages.</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Chat;
|
|
@ -0,0 +1,12 @@
|
|||
import NavBar from "../components/NavBar";
|
||||
|
||||
const Create = () => {
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<p>TODO create character page</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Create;
|
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
useLoaderData
|
||||
} from "react-router-dom";
|
||||
|
||||
import { Grid, GridItem, Heading } from "@chakra-ui/react";
|
||||
import NavBar from "../components/NavBar";
|
||||
import CharacterCard from "../components/CharacterCard";
|
||||
import { getCharacters } from "../api/characters";
|
||||
|
||||
export async function loader(){
|
||||
const characters = await getCharacters();
|
||||
return { characters };
|
||||
}
|
||||
|
||||
const Root = () => {
|
||||
const { characters } = useLoaderData();
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<Grid
|
||||
templateColumns={["repeat(1, 1fr)", "repeat(2, 1fr)", "repeat(4, 1fr)", "repeat(6, 1fr)", "repeat(8, 1fr)"]}
|
||||
ml="1.5rem"
|
||||
mr="1.5rem"
|
||||
gridGap="1rem"
|
||||
>
|
||||
<GridItem colStart={1} colEnd={-1}>
|
||||
<Heading size="md" noOfLines={1}>Characters</Heading>
|
||||
</GridItem>
|
||||
{characters.length ? (
|
||||
characters.map((character) => (
|
||||
<GridItem key={character.id}>
|
||||
<CharacterCard
|
||||
charId={character.id}
|
||||
image={character.image}
|
||||
alt={"Image of " + character.name}
|
||||
name={character.name}
|
||||
creator={character.creator}
|
||||
description={character.description}
|
||||
/>
|
||||
</GridItem>
|
||||
))
|
||||
) : (
|
||||
<GridItem>
|
||||
<p>No characters found!</p>
|
||||
</GridItem>
|
||||
)}
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Root;
|
|
@ -0,0 +1,12 @@
|
|||
import NavBar from "../components/NavBar";
|
||||
|
||||
const Search = () => {
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<p>TODO search page</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
|
@ -0,0 +1,10 @@
|
|||
import { extendTheme } from "@chakra-ui/react";
|
||||
|
||||
const config = {
|
||||
initialColorMode: "dark",
|
||||
useSystemColorMode: false
|
||||
};
|
||||
|
||||
const theme = extendTheme({ config });
|
||||
|
||||
export default theme;
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
Loading…
Reference in New Issue