Initial, working example.
This commit is contained in:
commit
f3e5cd62b1
26 changed files with 6597 additions and 0 deletions
19
.eslintrc.cjs
Normal file
19
.eslintrc.cjs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": ["error", { "allowTypedFunctionExpressions": false }],
|
||||||
|
},
|
||||||
|
}
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -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?
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Typescript react and MUI demo: [➡️ The actual interesting parts ⬅️](src/App.tsx)
|
16
index.html
Normal file
16
index.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>FrostByte</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
3448
package-lock.json
generated
Normal file
3448
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
34
package.json
Normal file
34
package.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "mui-practice",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.1",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/icons-material": "^5.14.11",
|
||||||
|
"@mui/material": "^5.14.11",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"sass": "^1.68.0",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5",
|
||||||
|
"vite-plugin-qrcode": "^0.2.2"
|
||||||
|
}
|
||||||
|
}
|
1
server/.gitignore
vendored
Executable file
1
server/.gitignore
vendored
Executable file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
2308
server/Cargo.lock
generated
Executable file
2308
server/Cargo.lock
generated
Executable file
File diff suppressed because it is too large
Load diff
17
server/Cargo.toml
Normal file
17
server/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = "4.4.0"
|
||||||
|
clap = { version = "4.4.5", features = ["derive"] }
|
||||||
|
env_logger = "0.10.0"
|
||||||
|
log = "0.4.20"
|
||||||
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
|
serde_json = "1.0.107"
|
||||||
|
sled = { version = "0.34.7" }
|
||||||
|
sqlx = "0.7.2"
|
||||||
|
uuid = { version = "1.4.1", features = ["serde", "v4"] }
|
32
server/src/main.rs
Executable file
32
server/src/main.rs
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use actix_web::{web::scope, App, HttpServer};
|
||||||
|
// use uuid::Uuid;
|
||||||
|
|
||||||
|
mod routes;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use routes::{get_posts, new_post, test};
|
||||||
|
use types::AppState;
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
|
||||||
|
info!("Starting server...");
|
||||||
|
|
||||||
|
let data = AppState::new();
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new().service(
|
||||||
|
scope("api")
|
||||||
|
.service(get_posts)
|
||||||
|
.service(new_post)
|
||||||
|
.service(routes::vote)
|
||||||
|
.service(test)
|
||||||
|
.app_data(Data::new(data.clone())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.bind("localhost:8080")?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
88
server/src/routes.rs
Executable file
88
server/src/routes.rs
Executable file
|
@ -0,0 +1,88 @@
|
||||||
|
use crate::types::{AppState, NewPost, Post};
|
||||||
|
use actix_web::web::{Data, Path};
|
||||||
|
use actix_web::{get, post, web::Json, HttpResponse, Responder};
|
||||||
|
use log::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub async fn get_posts(data: Data<AppState>) -> impl Responder {
|
||||||
|
match data.posts.lock() {
|
||||||
|
Ok(posts) => {
|
||||||
|
let posts: Vec<Post> = posts.values().cloned().collect();
|
||||||
|
HttpResponse::Ok().json(posts)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error: {:?}", e);
|
||||||
|
HttpResponse::InternalServerError().body("Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/")]
|
||||||
|
pub async fn new_post(new_post: Json<NewPost>, data: Data<AppState>) -> impl Responder {
|
||||||
|
let post = Post::from(new_post.into_inner());
|
||||||
|
info!("Created post {:?}", post.uuid);
|
||||||
|
|
||||||
|
match data.posts.lock() {
|
||||||
|
Ok(mut posts) => {
|
||||||
|
posts.insert(post.uuid, post);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error: {:?}", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpResponse::Ok().json("Post added!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a test route, returns "Hello, world!"
|
||||||
|
#[get("/test")]
|
||||||
|
pub async fn test(data: Data<AppState>) -> impl Responder {
|
||||||
|
match data.posts.lock() {
|
||||||
|
Ok(posts) => {
|
||||||
|
let posts: Vec<Post> = posts.values().cloned().collect();
|
||||||
|
HttpResponse::Ok().body(format!("Hello, world! {:?}", posts))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error: {:?}", e);
|
||||||
|
HttpResponse::InternalServerError().body("Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum VoteDirection {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Unupvote,
|
||||||
|
Undownvote,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("vote/{uuid}/{direction}")]
|
||||||
|
pub async fn vote(params: Path<(Uuid, VoteDirection)>, data: Data<AppState>) -> impl Responder {
|
||||||
|
let (uuid, direction) = params.into_inner();
|
||||||
|
println!("Voting {:?} on post {:?}", direction, uuid);
|
||||||
|
|
||||||
|
match data.posts.lock() {
|
||||||
|
Ok(mut posts) => {
|
||||||
|
let uuid = uuid;
|
||||||
|
if let Some(post) = posts.get_mut(&uuid) {
|
||||||
|
match direction {
|
||||||
|
VoteDirection::Up => post.votes.up += 1,
|
||||||
|
VoteDirection::Unupvote => post.votes.up -= 1,
|
||||||
|
VoteDirection::Down => post.votes.down += 1,
|
||||||
|
VoteDirection::Undownvote => post.votes.down -= 1,
|
||||||
|
}
|
||||||
|
HttpResponse::Ok().body("Downvoted!")
|
||||||
|
} else {
|
||||||
|
HttpResponse::NotFound().body("Post not found!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error: {:?}", e);
|
||||||
|
HttpResponse::InternalServerError().body("Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
server/src/types.rs
Executable file
56
server/src/types.rs
Executable file
|
@ -0,0 +1,56 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct NewPost {
|
||||||
|
title: String,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Post {
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub content: String,
|
||||||
|
pub votes: VoteCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NewPost> for Post {
|
||||||
|
fn from(post: NewPost) -> Self {
|
||||||
|
Self {
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
|
title: post.title,
|
||||||
|
content: post.content,
|
||||||
|
votes: VoteCount::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub posts: Arc<Mutex<BTreeMap<Uuid, Post>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
posts: Arc::new(Mutex::new(BTreeMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part of the post struct
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct VoteCount {
|
||||||
|
pub up: u32,
|
||||||
|
pub down: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VoteCount {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { up: 0, down: 0 }
|
||||||
|
}
|
||||||
|
}
|
29
src/App.tsx
Normal file
29
src/App.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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 [open, setOpen] = useState(false);
|
||||||
|
// const loginContextData = { open, setOpen };
|
||||||
|
// const loginContext = createContext(loginContextData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoginContext.Provider value={{ loginModalOpen: open, setOpen }} >
|
||||||
|
<Box flexDirection={"column"} display={"flex"} sx={{ width: "100%", minHeight: "100vh", backgroundColor:"background.default"}}>
|
||||||
|
<Header />
|
||||||
|
<LoginDialog />
|
||||||
|
<Primary />
|
||||||
|
<Footer />
|
||||||
|
</Box>
|
||||||
|
</LoginContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
16
src/Context.tsx
Normal file
16
src/Context.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { createContext } from "react"
|
||||||
|
|
||||||
|
export const TestContext = createContext("Test123")
|
||||||
|
|
||||||
|
// export const loginContextData = { open: false, setOpen: (open: boolean): void => {} };
|
||||||
|
|
||||||
|
interface LoginCTX {
|
||||||
|
loginModalOpen: boolean;
|
||||||
|
setOpen?: (open: boolean) => void;
|
||||||
|
currentUser?: string;
|
||||||
|
userToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginContextData = { loginModalOpen: false };
|
||||||
|
|
||||||
|
export const LoginContext = createContext<LoginCTX>(loginContextData);
|
36
src/Footer.tsx
Normal file
36
src/Footer.tsx
Normal 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;
|
90
src/Header.tsx
Normal file
90
src/Header.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { AppBar, Button, ButtonGroup, 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")
|
||||||
|
console.log(loginCtx);
|
||||||
|
loginCtx.setOpen?.(true); // If the loginCtx.setOpen is defined, call it with true as the argument
|
||||||
|
console.log(loginCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginCtx.currentUser != undefined) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: 1, display: "flex", flexDirection: "column", justifyContent: "center", ...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={{ width: 1, display: "flex", flexDirection: "column", justifyContent: "center", ...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 }}>
|
||||||
|
<Box sx={{ width: 1, px: 3, display: "flex", flexDirection: "row", flexGrow: 1, maxWidth: "1200px", justifyContent: "space-between" }}>
|
||||||
|
<HeaderLogo clickable/>
|
||||||
|
<NavButtons />
|
||||||
|
<LoginDisplay />
|
||||||
|
</Box>
|
||||||
|
</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: 1,
|
||||||
|
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;
|
64
src/LoginDialog.tsx
Normal file
64
src/LoginDialog.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
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 { useState } from "react";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { LoginContext } from "./Context";
|
||||||
|
// import { TestContext } from "./Context";
|
||||||
|
|
||||||
|
|
||||||
|
function LoginDialog(): JSX.Element {
|
||||||
|
// const [open, setOpen] = useState(openState);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
// const test = useContext(TestContext);
|
||||||
|
|
||||||
|
const loginCTX = useContext(LoginContext);
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
loginCTX.setOpen?.(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = (): void => {
|
||||||
|
console.log(username, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginDialog;
|
99
src/Post.tsx
Normal file
99
src/Post.tsx
Normal 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(URL + '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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
86
src/Primary.tsx
Normal file
86
src/Primary.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
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("http://localhost:8080/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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function PostInput({ newPostCallback }: { newPostCallback: () => void }): JSX.Element {
|
||||||
|
const [cinput, setCinput] = useState("");
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = (): void => {
|
||||||
|
if (cinput && title) submitPostToServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitPostToServer = async (): Promise<void> => {
|
||||||
|
const newPost = { title: title, content: cinput };
|
||||||
|
|
||||||
|
const response = await fetch("http://localhost:8080/api/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newPost)
|
||||||
|
})
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
|
newPostCallback();
|
||||||
|
setCinput("");
|
||||||
|
setTitle("");
|
||||||
|
}
|
||||||
|
|
||||||
|
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={cinput} fullWidth onChange={(a): void => setCinput(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
src/Theme.tsx
Normal file
24
src/Theme.tsx
Normal 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
src/main.tsx
Normal file
22
src/main.tsx
Normal 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>,
|
||||||
|
)
|
43
src/style/index.scss
Normal file
43
src/style/index.scss
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// :root {
|
||||||
|
// font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
// line-height: 1.5;
|
||||||
|
// font-weight: 400;
|
||||||
|
|
||||||
|
// color-scheme: light dark;
|
||||||
|
// color: rgba(255, 255, 255, 0.87);
|
||||||
|
// background-color: #242424;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// body * {
|
||||||
|
// box-sizing: border-box;
|
||||||
|
// margin: 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// max-width: 1280px;
|
||||||
|
// margin: 0 auto;
|
||||||
|
// padding: 2rem;
|
||||||
|
// text-align: center;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // @media (prefers-reduced-motion: no-preference) {}
|
||||||
|
|
||||||
|
// body {
|
||||||
|
// margin: 0;
|
||||||
|
// // display: flex;
|
||||||
|
// min-width: 320px;
|
||||||
|
// min-height: 100vh;
|
||||||
|
// place-items: center;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @media (prefers-color-scheme: light) {
|
||||||
|
// :root {
|
||||||
|
// color: #213547;
|
||||||
|
// background-color: #ffffff;
|
||||||
|
// }
|
||||||
|
// a:hover {
|
||||||
|
// color: #747bff;
|
||||||
|
// }
|
||||||
|
// button {
|
||||||
|
// background-color: #f9f9f9;
|
||||||
|
// }
|
||||||
|
// }
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// / <reference types="vite/client" />
|
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
8
vite.config.ts
Normal file
8
vite.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import { qrcode } from 'vite-plugin-qrcode'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), qrcode()],
|
||||||
|
})
|
Loading…
Reference in a new issue