Restructure

This commit is contained in:
Imbus 2023-10-10 17:55:30 +02:00
parent d657ee600a
commit 7d7845441d
18 changed files with 0 additions and 0 deletions

41
client/src/App.tsx Normal file
View file

@ -0,0 +1,41 @@
import Box from '@mui/material/Box'
import { useState } from 'react'
import Header from './Header.tsx'
import Primary from './Primary.tsx';
import Footer from './Footer.tsx';
import LoginDialog from './LoginDialog.tsx';
import { LoginContext } from './Context.tsx';
// JSX.Element is the return type of every React component
function App(): JSX.Element {
const [loginModalOpen, setLoginModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState<string | undefined>(undefined);
const [userToken, setUserToken] = useState<string | undefined>(undefined);
const loginContextData = {
loginModalOpen: loginModalOpen,
currentUser: currentUser,
userToken: userToken,
setOpen: setLoginModalOpen,
setCurrentUser: setCurrentUser,
setUserToken: setUserToken,
};
// const loginContextData = { open, setOpen };
// const loginContext = createContext(loginContextData);
return (
<LoginContext.Provider value={loginContextData} >
<Box flexDirection={"column"} display={"flex"} sx={{ width: "100%", minHeight: "100vh", backgroundColor:"background.default"}}>
<Header />
<LoginDialog />
<Primary />
<Footer />
</Box>
</LoginContext.Provider>
)
}
export default App;

23
client/src/Context.tsx Normal file
View file

@ -0,0 +1,23 @@
import { createContext } from "react"
export const TestContext = createContext("Test123")
interface LoginCTX {
loginModalOpen: boolean;
currentUser?: string;
userToken?: string;
setOpen?: (open: boolean) => void;
setCurrentUser?: (username: string) => void;
setUserToken?: (token: string) => void;
}
const loginContextData = {
loginModalOpen: false,
currentUser: undefined,
userToken: undefined,
setOpen: undefined,
setCurrentUser: undefined,
setUserToken: undefined,
};
export const LoginContext = createContext<LoginCTX>(loginContextData);

36
client/src/Footer.tsx Normal file
View file

@ -0,0 +1,36 @@
// import React from "react";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import { Grid } from "@mui/material";
function Footer(): JSX.Element {
return (
<Box sx={{
flexGrow: "1",
display: "flex",
flexDirection: "column",
backgroundColor: "#242424",
// backgroundColor: "background.paper",
color: "text.secondary"
}}>
<Grid container sx={{ mt: 6, maxWidth: "1000px", width: "100%", mx: "auto" }}>
<Container sx={{textAlign: "center"}}>
<Typography color="text.secondary" gutterBottom sx={{userSelect: "none",opacity:"40%"}}>
Δ DeltaLabs {new Date().getFullYear()}
</Typography>
</Container>
{/* { ["Tasteful", "Looking", "Footer"].map((letter): JSX.Element => {
return (
<Grid key={letter} item xs={12} sm={6} md={4} lg={4} sx={{textAlign: "center"}}>
<Typography>{letter}</Typography>
</Grid>
)
}) } */}
</Grid>
</Box>
)
}
export default Footer;

93
client/src/Header.tsx Normal file
View file

@ -0,0 +1,93 @@
import { AppBar, Button, ButtonGroup, Grid, Link, Typography } from "@mui/material";
import Box from "@mui/material/Box";
import AcUnitIcon from '@mui/icons-material/AcUnit';
import { cyan } from "@mui/material/colors";
import { useContext } from "react";
import { LoginContext } from "./Context";
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import HomeIcon from '@mui/icons-material/Home';
import PostAddIcon from '@mui/icons-material/PostAdd';
function LoginDisplay({ sx }: { sx?: React.CSSProperties }): JSX.Element {
const loginCtx = useContext(LoginContext);
const handleLogin = (): void => {
console.log("Login button pressed")
if (loginCtx.currentUser == undefined) {
loginCtx.setOpen?.(true); // If the loginCtx.setOpen is defined, call it with true as the argument
} else {
loginCtx.setCurrentUser?.(undefined);
loginCtx.setUserToken?.(undefined);
}
}
if (loginCtx.currentUser != undefined) {
return (
<Box sx={{ textAlign: "right", ...sx }}>
<Typography color={"#808080"} sx={{ textAlign: "right" }}>
Logged in as:
</Typography>
<Typography sx={{ textAlign: "right" }}>
<Link href='#' underline='hover' onClick={(): void => handleLogin()}>
{loginCtx.currentUser}
</Link>
</Typography>
</Box>
)
}
return (
<Box sx={{ textAlign: "right", ...sx }}>
<Button variant="text" startIcon={<AccountCircleIcon />} onClick={(): void => handleLogin()}>Login</Button>
</Box>
)
}
function Header({ sx }: { sx?: React.CSSProperties }): JSX.Element {
return (
<AppBar position='static' sx={{ p: 1, px: 3, display: 'flex', flexDirection: 'row', justifyContent: 'space-evenly', ...sx }}>
<Grid container px={2} spacing={2} sx={{ flexGrow: 1, display: "flex", maxWidth: "1200px", flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
<Grid item xs={12} sm={6} md={4} lg={4}><HeaderLogo clickable /></Grid>
<Grid item xs={12} sm={6} md={4} lg={4}><NavButtons /></Grid>
<Grid item xs={12} sm={6} md={4} lg={4}><LoginDisplay /></Grid>
</Grid>
</AppBar >)
}
function HeaderLogo({ clickable, sx }: { clickable?: boolean, sx?: React.CSSProperties }): JSX.Element {
const clickStyle = {
transition: "0.3s all ease-in-out",
":hover": { textShadow: "0 0px 15px" + cyan[400] }
}
return (
<Box sx={{
width: "fit-content",
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
userSelect: "none",
cursor: "pointer",
...clickable ? clickStyle : {},
...sx
}}>
<AcUnitIcon fontSize="large" sx={{ color: cyan[400], mr: 1 }}></AcUnitIcon>
<Typography variant='h4'>FrostByte</Typography>
</Box>
)
}
function NavButtons({ sx }: { sx?: React.CSSProperties }): JSX.Element {
return (
<Box sx={{ display: 'flex', flexDirection: 'row', maxWidth: "400px", width: 1, ...sx }}>
<ButtonGroup variant="text" aria-label="text button group" sx={{ width: "100%" }}>
{["Home", "New"].map((typename): JSX.Element => {
return (
<Button startIcon={typename === "Home" ? <HomeIcon /> : <PostAddIcon />} key={typename} sx={{ px: 2, bgcolor: "#FFFFFF15", width: 1 }}>{typename}</Button>
)
})}
</ButtonGroup>
</Box>)
}
export default Header;

132
client/src/LoginDialog.tsx Normal file
View file

@ -0,0 +1,132 @@
import { Dialog } from "@mui/material";
import { DialogTitle } from "@mui/material";
import { DialogContent } from "@mui/material";
import { DialogContentText } from "@mui/material";
import { DialogActions } from "@mui/material";
import { Button } from "@mui/material";
import { TextField } from "@mui/material";
import { useEffect, useState } from "react";
import { useContext } from "react";
import { LoginContext } from "./Context";
// import { TestContext } from "./Context";
interface RegisterData {
username: string,
password: string,
captcha: string,
}
interface LoginData {
username: string,
password: string,
}
function sendRegister(data: RegisterData): void {
console.log(JSON.stringify(data));
fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
}
interface LoginResponse {
username: string,
token: string,
}
async function sendLogin(data: LoginData): Promise<LoginResponse> {
// console.log(JSON.stringify(data));
const response_promise = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
const logindata = await response_promise.json();
return (logindata as LoginResponse);
}
function LoginDialog(): JSX.Element {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const loginCTX = useContext(LoginContext);
const setLoggedInAs = (username: string, token: string): void => {
loginCTX.setCurrentUser?.(username);
loginCTX.setUserToken?.(token);
handleClose();
}
// const setLogOut = (): void => {
// loginCTX.setCurrentUser?.(undefined);
// loginCTX.setUserToken?.(undefined);
// localStorage.removeItem("loginState");
// }
const handleClose = (): void => {
loginCTX.setOpen?.(false);
setUsername("");
setPassword("");
};
// Check for localStorage login state
useEffect((): void => {
const loginState = JSON.parse(localStorage.getItem("loginState")||"{}");
if (loginState.username && loginState.token) {
setLoggedInAs(loginState.username, loginState.token);
}
}, []);
const handleLogin = async (): Promise<void> => {
const response = await sendLogin({ username: username, password: password });
if (response && response.username && response.token) {
setLoggedInAs(response.username, response.token);
localStorage.setItem("loginState", JSON.stringify(response));
}
};
const handleRegister = (): void => {
sendRegister({ username: username, password: password, captcha: "test" });
}
return (
<Dialog open={loginCTX.loginModalOpen} onClose={handleClose} sx={{ p: 2, top: "-40%" }}>
<DialogTitle>Login</DialogTitle>
<DialogContent>
<DialogContentText>
Please enter your username and password.
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="username"
label="Username"
type="text"
fullWidth
value={username}
onChange={(e): void => setUsername(e.target.value)}
/>
<TextField
margin="dense"
id="password"
label="Password"
type="password"
fullWidth
value={password}
onChange={(e): void => setPassword(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleLogin}>Login</Button>
</DialogActions>
<DialogActions>
<Button onClick={handleRegister}>Register</Button>
</DialogActions>
</Dialog>
);
}
export default LoginDialog;

99
client/src/Post.tsx Normal file
View file

@ -0,0 +1,99 @@
import { Card, CardActions, CardContent, IconButton, Typography } from "@mui/material";
import { ArrowDownward, ArrowUpward, Comment, Share } from "@mui/icons-material";
import { useState } from "react";
export interface Post {
uuid: string,
title: string,
content: string,
votes: { up: number, down: number },
}
// const URL = "http://localhost:8080/api/";
function sendVote(post: Post, direction: string): void {
fetch('/api/vote/' + post.uuid + '/' + direction, { method: 'POST' });
}
enum VoteDirection { UP = 1, DOWN = -1, NONE = 0 }
// // Single post
export function PostCard({ post }: { post: Post }): JSX.Element {
// const [myVote, setMyVote] = useState({ up: 0, down: 0 });
const [myVote, setMyVote] = useState(VoteDirection.NONE);
const [voteCount, setVoteCount] = useState({
upvotes: post.votes.up,
downvotes: post.votes.down
});
// Atrocious code
const votePress = (vote: VoteDirection): void => {
console.log(vote);
if (vote === VoteDirection.UP) {
if (myVote === VoteDirection.NONE) { // Upvote
sendVote(post, 'up');
setMyVote(VoteDirection.UP);
}
else if (myVote === VoteDirection.UP) { // Unvote
sendVote(post, 'unupvote');
setMyVote(VoteDirection.NONE);
}
else if (myVote === VoteDirection.DOWN) { // Change vote
sendVote(post, 'undownvote');
sendVote(post, 'up');
setMyVote(VoteDirection.UP);
}
}
if (vote === VoteDirection.DOWN) {
if (myVote === VoteDirection.NONE) { // Downvote
sendVote(post, 'down');
setMyVote(VoteDirection.DOWN);
}
else if (myVote === VoteDirection.DOWN) { // Unvote
sendVote(post, 'undownvote');
setMyVote(VoteDirection.NONE);
}
else if (myVote === VoteDirection.UP) { // Change vote
sendVote(post, 'unupvote');
sendVote(post, 'down');
setMyVote(VoteDirection.DOWN);
}
}
}
return (
<>
<Card key={post.uuid} sx={{ width: 1, bgcolor: "background.default" }}>
<CardContent>
<Typography variant="h6">
{post.title}
</Typography>
<Typography sx={{ overflowWrap: 'break-word' }} variant="body2">
{post.content ? post.content : "No content"}
</Typography>
</CardContent>
<CardActions>
<IconButton size='small' color='primary' aria-label="Comment"><Comment /></IconButton>
<IconButton color={myVote > 0 ? 'success' : 'default'}
onClick={(): void => votePress(VoteDirection.UP)} size='small' aria-label="Upvote"><ArrowUpward />
{voteCount.upvotes + Math.max(myVote, 0)}
</IconButton>
<IconButton color={myVote < 0 ? 'secondary' : 'default'}
onClick={(): void => votePress(VoteDirection.DOWN)} size='small' aria-label="Downvote"><ArrowDownward />
{voteCount.downvotes + Math.max(-myVote, 0)}
</IconButton>
<div style={{ marginLeft: 'auto' }}>
<IconButton size='small' aria-label="Share"><Share /></IconButton>
</div>
</CardActions>
</Card>
</>
)
}

88
client/src/Primary.tsx Normal file
View file

@ -0,0 +1,88 @@
import { Box, Container, Grid, Button, Paper, TextField, FormControl } from "@mui/material";
import { useEffect, useState } from "react";
import SendIcon from '@mui/icons-material/Send';
import CancelIcon from '@mui/icons-material/Cancel';
import { Post, PostCard } from "./Post";
function Primary(): JSX.Element {
const [posts, setPosts] = useState<Post[]>([]);
const refreshPosts = (): void => {
fetch("/api/").then((response): void => {
response.json().then((data): void => {
setPosts(data);
})
})
}
useEffect((): void => {
refreshPosts();
}, [])
return (
<Container sx={{ p: 2, display: "flex", flexDirection: "column", alignItems: "center", backgroundColor: "background.default", minHeight: "60vh" }}>
<PostInput newPostCallback={refreshPosts} />
<PostContainer posts={posts} />
</Container>
)
}
function PostContainer({ posts }: { posts: Post[] }): JSX.Element {
return (
<Grid container justifyContent={"center"} spacing={2} pb={2}>
{posts.map((p): JSX.Element => {
return (
<Grid item xs={12} sm={12} md={6} lg={6} xl={6} key={p.uuid}>
<PostCard key={p.uuid} post={p} />
</Grid>
)
})}
</Grid>
)
}
interface NewPost {
content: string,
token: string,
}
function PostInput({ newPostCallback }: { newPostCallback: () => void }): JSX.Element {
const [currentInput, setCurrentInput] = useState("");
const handleSubmit = (): void => {
if (currentInput) submitPostToServer();
}
const submitPostToServer = async (): Promise<void> => {
const newPost: NewPost = { content: currentInput, token: "" }
const response = await fetch("/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(newPost)
})
const data = await response.json();
console.log(data);
newPostCallback();
setCurrentInput("");
}
return (
<Box py={2} display={"flex"} flexDirection={"column"} width={1}>
<Paper sx={{ p: 2, display: "flex", flexDirection: "column", alignContent: "center", alignItems: "center" }} elevation={4}>
<FormControl fullWidth>
{/* <TextField sx={{pb:1}} size="small" id="outlined-basic" label="Title" variant="outlined" value={title} fullWidth onChange={(a): void => setTitle(a.target.value)} /> */}
<TextField multiline id="outlined-basic" label="Share your thoughts" variant="outlined" minRows={4} value={currentInput} fullWidth onChange={(a): void => setCurrentInput(a.target.value)} />
<Container disableGutters sx={{ mt:1, display: "flex", flexDirection: "row", justifyContent: "flex-end", alignItems: "center", width: "100%" }}>
<Button sx={{mr:1}} type="reset" startIcon={<CancelIcon/>} variant="outlined" onClick={handleSubmit}>Cancel</Button>
<Button type="submit" startIcon={<SendIcon />} variant="contained" onClick={handleSubmit}>Send</Button>
</Container>
</FormControl>
</Paper>
</Box>
)
}
export default Primary;

24
client/src/Theme.tsx Normal file
View file

@ -0,0 +1,24 @@
import { createTheme } from "@mui/material";
const theme = createTheme({
palette: {
mode: "dark",
// primary: {
// main: "#ff0000",
// },
// secondary: {
// main: "#00ff00",
// },
},
typography: {
fontFamily: "Roboto, sans-serif",
// h1: {
// fontSize: "3.5rem",
// },
// h3: {
// fontSize: "2rem",
// },
},
});
export default theme;

22
client/src/main.tsx Normal file
View file

@ -0,0 +1,22 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './style/index.scss'
import theme from './Theme.tsx'
import { ThemeProvider } from '@mui/material'
import { CssBaseline } from '@mui/material'
import { TestContext } from './Context.tsx'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<CssBaseline />
<ThemeProvider theme={theme}>
<TestContext.Provider value="Test123">
<App />
</TestContext.Provider>
</ThemeProvider>
<CssBaseline />
</React.StrictMode>,
)

View file

@ -0,0 +1 @@
// No free standing CSS

1
client/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
// / <reference types="vite/client" />