Working on upload images and tilemap
This commit is contained in:
@@ -29,6 +29,7 @@ jsonwebtoken = "9.0.0"
|
|||||||
redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] }
|
redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] }
|
||||||
base64 = "0.21.4"
|
base64 = "0.21.4"
|
||||||
rust-s3 = "0.33.0"
|
rust-s3 = "0.33.0"
|
||||||
|
actix-multipart = "0.6.1"
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.32.0"
|
version = "1.32.0"
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
last_name TEXT NOT NULL,
|
last_name TEXT NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
profile TEXT,
|
||||||
verified BOOLEAN NOT NULL DEFAULT FALSE
|
verified BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
);
|
);
|
||||||
@@ -29,6 +29,7 @@ impl RegisterUser {
|
|||||||
last_name: self.last_name,
|
last_name: self.last_name,
|
||||||
updated_at: chrono::Utc::now().naive_utc(),
|
updated_at: chrono::Utc::now().naive_utc(),
|
||||||
created_at: chrono::Utc::now().naive_utc(),
|
created_at: chrono::Utc::now().naive_utc(),
|
||||||
|
profile: None,
|
||||||
verified: false,
|
verified: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,7 @@ pub struct QueryUser {
|
|||||||
pub last_name: String,
|
pub last_name: String,
|
||||||
pub updated_at: chrono::NaiveDateTime,
|
pub updated_at: chrono::NaiveDateTime,
|
||||||
pub created_at: chrono::NaiveDateTime,
|
pub created_at: chrono::NaiveDateTime,
|
||||||
|
pub profile: Option<String>,
|
||||||
pub verified: bool,
|
pub verified: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ pub struct InsertUser {
|
|||||||
pub last_name: String,
|
pub last_name: String,
|
||||||
pub updated_at: chrono::NaiveDateTime,
|
pub updated_at: chrono::NaiveDateTime,
|
||||||
pub created_at: chrono::NaiveDateTime,
|
pub created_at: chrono::NaiveDateTime,
|
||||||
|
pub profile: Option<String>,
|
||||||
pub verified: bool,
|
pub verified: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +89,15 @@ impl InsertUser {
|
|||||||
.get_result(&mut conn)?;
|
.get_result(&mut conn)?;
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_profile(email: &str, profile: Option<&str>) -> Result<QueryUser, ServiceError> {
|
||||||
|
let mut conn = connection()?;
|
||||||
|
let user = diesel::update(users::table)
|
||||||
|
.filter(users::email.eq(&email))
|
||||||
|
.set(users::profile.eq(profile))
|
||||||
|
.get_result(&mut conn)?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -114,7 +114,12 @@ impl From<redis::RedisError> for ServiceError {
|
|||||||
|
|
||||||
impl From<s3::error::S3Error> for ServiceError {
|
impl From<s3::error::S3Error> for ServiceError {
|
||||||
fn from(error: s3::error::S3Error) -> ServiceError {
|
fn from(error: s3::error::S3Error) -> ServiceError {
|
||||||
ServiceError::new(500, format!("Unknown s3 error: {}", error))
|
match error {
|
||||||
|
s3::error::S3Error::Http(code, message) => {
|
||||||
|
ServiceError::new(code, message)
|
||||||
|
},
|
||||||
|
_ => ServiceError::new(500, format!("Unknown s3 error: {}", error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ mod auth;
|
|||||||
mod dnd;
|
mod dnd;
|
||||||
mod bot;
|
mod bot;
|
||||||
mod storage;
|
mod storage;
|
||||||
|
mod users;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
@@ -125,8 +126,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.app_data(web::Data::new(Arc::clone(&app_data)))
|
.app_data(web::Data::new(Arc::clone(&app_data)))
|
||||||
.configure(crate::storage::messages::init_routes)
|
.configure(crate::storage::messages::init_routes)
|
||||||
.configure(crate::dnd::spells::init_routes)
|
|
||||||
.configure(crate::auth::init_routes)
|
.configure(crate::auth::init_routes)
|
||||||
|
.configure(crate::users::init_routes)
|
||||||
|
.configure(crate::dnd::spells::init_routes)
|
||||||
.configure(crate::bot::api::init_routes)
|
.configure(crate::bot::api::init_routes)
|
||||||
})
|
})
|
||||||
.bind(format!("{}:{}", host, port)) {
|
.bind(format!("{}:{}", host, port)) {
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ pub async fn upload_file(path: &str, content: &[u8]) -> Result<ResponseData, Ser
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_file(path: &str) -> Result<Vec<u8>, ServiceError> {
|
||||||
|
let response = BUCKET.get_object(path).await?;
|
||||||
|
let bytes = response.bytes();
|
||||||
|
Ok(bytes.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn delete_file(path: &str) -> Result<ResponseData, ServiceError> {
|
pub async fn delete_file(path: &str) -> Result<ResponseData, ServiceError> {
|
||||||
let response = BUCKET.delete_object(path).await?;
|
let response = BUCKET.delete_object(path).await?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ diesel::table! {
|
|||||||
last_name -> Text,
|
last_name -> Text,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Timestamp,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
|
profile -> Nullable<Text>,
|
||||||
verified -> Bool,
|
verified -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3
service/src/users/mod.rs
Normal file
3
service/src/users/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod routes;
|
||||||
|
|
||||||
|
pub use routes::init_routes;
|
||||||
136
service/src/users/routes.rs
Normal file
136
service/src/users/routes.rs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
use actix_multipart::Multipart;
|
||||||
|
use actix_web::{web, HttpResponse, post, delete, get, ResponseError};
|
||||||
|
use log::error;
|
||||||
|
use serenity::futures::StreamExt;
|
||||||
|
use siren::ServiceError;
|
||||||
|
|
||||||
|
use crate::{auth::{JwtAuth, InsertUser, QueryUser}, storage::{upload_file, get_file, delete_file}};
|
||||||
|
|
||||||
|
#[post("/picture")]
|
||||||
|
async fn set_picture(mut payload: Multipart, auth: JwtAuth) -> HttpResponse {
|
||||||
|
while let Some(item) = payload.next().await {
|
||||||
|
let mut bytes = web::BytesMut::new();
|
||||||
|
let mut field = match item {
|
||||||
|
Ok(field) => field,
|
||||||
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
|
};
|
||||||
|
let content_type = field.content_disposition();
|
||||||
|
// Get file name and construct the file path
|
||||||
|
let file_name = match content_type.get_filename() {
|
||||||
|
Some(name) => {
|
||||||
|
// Verify extension is supported
|
||||||
|
match name.split(".").last() {
|
||||||
|
Some(ext) => {
|
||||||
|
match ext {
|
||||||
|
"png" | "jpg" | "jpeg" => name,
|
||||||
|
_ => return ResponseError::error_response(&ServiceError {
|
||||||
|
status: 400,
|
||||||
|
message: "File extension is not supported".to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => return ResponseError::error_response(&ServiceError {
|
||||||
|
status: 400,
|
||||||
|
message: "Unknown file extension".to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => return ResponseError::error_response(&ServiceError {
|
||||||
|
status: 400,
|
||||||
|
message: "File name is not provided".to_string()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let path = format!("users/{}/{}", auth.user.email, file_name);
|
||||||
|
|
||||||
|
// Build the file and store it in minio
|
||||||
|
while let Some(chunk) = field.next().await {
|
||||||
|
let data = match chunk {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to get chunk: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
bytes.extend_from_slice(&data);
|
||||||
|
}
|
||||||
|
match upload_file(&path, &bytes).await {
|
||||||
|
Ok(_) => {
|
||||||
|
match InsertUser::update_profile(&auth.user.email, Some(&path)) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to update user profile: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to upload file: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return HttpResponse::Ok().finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/picture")]
|
||||||
|
async fn get_picture(auth: JwtAuth) -> HttpResponse {
|
||||||
|
let user = match QueryUser::get_by_email(&auth.user.email) {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to get user: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(path) = user.profile {
|
||||||
|
match get_file(&path).await {
|
||||||
|
Ok(bytes) => return HttpResponse::Ok().body(bytes),
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to get file: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return HttpResponse::NotFound().finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/picture")]
|
||||||
|
async fn delete_picture(auth: JwtAuth) -> HttpResponse {
|
||||||
|
match QueryUser::get_by_email(&auth.user.email) {
|
||||||
|
Ok(user) => {
|
||||||
|
match user.profile {
|
||||||
|
Some(path) => {
|
||||||
|
match delete_file(&path).await {
|
||||||
|
Ok(_) => {
|
||||||
|
match InsertUser::update_profile(&auth.user.email, None) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to update user profile: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to delete file: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to get user: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return HttpResponse::Ok().finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||||
|
config.service(web::scope("users")
|
||||||
|
.service(set_picture)
|
||||||
|
.service(get_picture)
|
||||||
|
.service(delete_picture)
|
||||||
|
);
|
||||||
|
}
|
||||||
1278
ui/package-lock.json
generated
1278
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,31 +9,30 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^7.1.2",
|
"@mantine/core": "^7.1.5",
|
||||||
"@mantine/form": "^7.1.2",
|
"@mantine/form": "^7.1.5",
|
||||||
"@mantine/hooks": "^7.1.2",
|
"@mantine/hooks": "^7.1.5",
|
||||||
"@mantine/modals": "^7.1.2",
|
"@mantine/modals": "^7.1.5",
|
||||||
"@mantine/notifications": "^7.1.2",
|
"@mantine/notifications": "^7.1.5",
|
||||||
"axios": "^1.5.1",
|
"@pixi/react": "^7.1.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"next": "^13.5.4",
|
"next": "^13.5.6",
|
||||||
|
"pixi.js": "^7.3.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-leaflet": "^4.2.1",
|
|
||||||
"recharts": "^2.8.0",
|
|
||||||
"recoil": "^0.7.7"
|
"recoil": "^0.7.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-cookie": "^3.0.4",
|
"@types/js-cookie": "^3.0.5",
|
||||||
"@types/node": "20.8.2",
|
"@types/node": "20.8.7",
|
||||||
"@types/react": "18.2.24",
|
"@types/react": "18.2.31",
|
||||||
"@types/react-dom": "18.2.8",
|
"@types/react-dom": "18.2.14",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@typescript-eslint/parser": "^6.8.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"eslint": "8.50.0",
|
"eslint": "8.52.0",
|
||||||
"eslint-config-next": "13.5.4",
|
"eslint-config-next": "13.5.6",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export async function logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function refresh(refresh_token_rotation?: boolean): Promise<ResponseAuth | undefined> {
|
export async function refresh(refresh_token_rotation?: boolean): Promise<ResponseAuth | undefined> {
|
||||||
const response = await get('auth/refresh', { params: { refresh_token_rotation } });
|
const response = await get('auth/refresh', { refresh_token_rotation });
|
||||||
if (response?.status === 200) {
|
if (response?.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export async function get(endpoint: string, params: Record<string, any> = {}): P
|
|||||||
// Remove undefined params
|
// Remove undefined params
|
||||||
Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]);
|
Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]);
|
||||||
const urlParams = new URLSearchParams(params);
|
const urlParams = new URLSearchParams(params);
|
||||||
const url = urlParams ? `${baseURL}/${endpoint}?${urlParams}` : `${baseURL}/${endpoint}`;
|
const url = urlParams && urlParams.size > 0 ? `${baseURL}/${endpoint}?${urlParams}` : `${baseURL}/${endpoint}`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
@@ -16,11 +16,15 @@ export async function get(endpoint: string, params: Record<string, any> = {}): P
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function post(endpoint: string, body = {}): Promise<Response> {
|
interface PostOptions {
|
||||||
|
headers?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function post(endpoint: string, body = {}, options?: PostOptions): Promise<Response> {
|
||||||
const url = `${baseURL}/${endpoint}`;
|
const url = `${baseURL}/${endpoint}`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: options?.headers || {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|||||||
24
ui/src/api/users.ts
Normal file
24
ui/src/api/users.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { get, post } from '.';
|
||||||
|
|
||||||
|
export async function getPicture(): Promise<Blob | undefined> {
|
||||||
|
const response = await get('users/picture');
|
||||||
|
if (response?.status === 200) {
|
||||||
|
return response.blob();
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setPicture(payload: File): Promise<boolean> {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('file', payload);
|
||||||
|
// TODO: Figure out why the form data object is empty
|
||||||
|
const response = await post('users/picture', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
if (response?.status === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,15 +22,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<head>
|
<head>
|
||||||
<title>Siren</title>
|
<title>Siren</title>
|
||||||
</head>
|
</head>
|
||||||
<body className={`${inter.className} wrapper h-full`}>
|
<body className={`${inter.className} wrapper`}>
|
||||||
<RecoilRootWrapper>
|
<RecoilRootWrapper>
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<ModalsProvider>
|
<ModalsProvider>
|
||||||
<Header />
|
<Header />
|
||||||
<Box p='xl' pt='sm' className='h-full'>
|
<Box>{children}</Box>
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</RecoilRootWrapper>
|
</RecoilRootWrapper>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import TileGrid from '@/components/TileGrid';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
// Home page for siren
|
// Home page for siren
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <div></div>;
|
return (
|
||||||
|
<div>
|
||||||
|
<TileGrid />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: black;
|
color: black;
|
||||||
border-bottom: 1px solid #e6e6e6;
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
max-height: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .left {
|
.navbar .left {
|
||||||
|
|||||||
@@ -3,15 +3,16 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import './header.css';
|
import './header.css';
|
||||||
import { Avatar, Button, Card, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core';
|
import { Avatar, Button, Card, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { logout, me, refreshLoggedIn } from '@/api/auth';
|
import { logout, refresh, refreshLoggedIn } from '@/api/auth';
|
||||||
import { useToggle } from '@mantine/hooks';
|
import { useToggle } from '@mantine/hooks';
|
||||||
import { HeaderModal } from './HeaderModal';
|
import { HeaderModal } from './HeaderModal';
|
||||||
import { HeaderItem, headerItems } from './headerItems';
|
import { HeaderItem, headerItems } from './headerItems';
|
||||||
import { userState } from '@/state/auth';
|
import { userState } from '@/state/auth';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { getPicture, setPicture } from '@/api/users';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const pathName = usePathname();
|
const pathName = usePathname();
|
||||||
@@ -19,13 +20,19 @@ export default function Header() {
|
|||||||
const [headers, setHeaders] = useState<HeaderItem[]>([]);
|
const [headers, setHeaders] = useState<HeaderItem[]>([]);
|
||||||
const [user, setUser] = useRecoilState(userState);
|
const [user, setUser] = useRecoilState(userState);
|
||||||
const [refreshId, setRefreshId] = useState<NodeJS.Timeout | undefined>(undefined);
|
const [refreshId, setRefreshId] = useState<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
const [profilePicture, setProfilePicture] = useState<File | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user && Cookies.get('logged_in')) {
|
if (!user || !Cookies.get('logged_in')) {
|
||||||
me().then((response) => {
|
refresh().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
setRefreshId(refreshLoggedIn());
|
setRefreshId(refreshLoggedIn());
|
||||||
setUser(response.user);
|
setUser(response.user);
|
||||||
|
getPicture().then((response) => {
|
||||||
|
if (response) {
|
||||||
|
setProfilePicture(response as File);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -62,27 +69,51 @@ export default function Header() {
|
|||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<UnstyledButton className='user user-button'>
|
<UnstyledButton className='user user-button'>
|
||||||
<Group>
|
<Group>
|
||||||
<Avatar />
|
<Avatar src={profilePicture ? URL.createObjectURL(profilePicture) : undefined} />
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Text size='sm' fw={500}>
|
<Text size='sm' fw={500}>
|
||||||
{user.first_name} {user.last_name}
|
{user.first_name} {user.last_name}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text c='dimmed' size='xs' style={{ textTransform: 'uppercase' }}>
|
||||||
<Text c='dimmed' size='xs'>
|
|
||||||
{user.role}
|
{user.role}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown p={0}>
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Section h={140} style={{}} />
|
<Card.Section h={140} style={{ backgroundColor: '#4481e3' }} />
|
||||||
<Avatar size={80} radius={80} mx={'auto'} mt={-30} />
|
<FileButton
|
||||||
|
onChange={(payload) => {
|
||||||
|
if (payload) {
|
||||||
|
setPicture(payload).then((response) => {
|
||||||
|
if (response) {
|
||||||
|
setProfilePicture(payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
accept='image/png,image/jpeg,image/jpg'
|
||||||
|
multiple={false}
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<Avatar
|
||||||
|
{...props}
|
||||||
|
component='button'
|
||||||
|
size={80}
|
||||||
|
radius={80}
|
||||||
|
mx={'auto'}
|
||||||
|
mt={-30}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
src={profilePicture ? URL.createObjectURL(profilePicture) : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
<Text ta='center' fz='lg' fw={500} mt='sm'>
|
<Text ta='center' fz='lg' fw={500} mt='sm'>
|
||||||
{user.first_name} {user.last_name}
|
{user.first_name} {user.last_name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta='center' fz='sm' c='dimmed'>
|
<Text ta='center' fz='sm' c='dimmed' style={{ textTransform: 'uppercase' }}>
|
||||||
{user.role}
|
{user.role}
|
||||||
</Text>
|
</Text>
|
||||||
<Grid mt='xl'>
|
<Grid mt='xl'>
|
||||||
|
|||||||
0
ui/src/components/TileGrid/Viewport.tsx
Normal file
0
ui/src/components/TileGrid/Viewport.tsx
Normal file
38
ui/src/components/TileGrid/index.tsx
Normal file
38
ui/src/components/TileGrid/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Graphics, Stage } from '@pixi/react';
|
||||||
|
import { Graphics as PixiGraphics } from '@pixi/graphics';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
// export default function TileGrid({ width, height }: TileGridProps) {
|
||||||
|
export default function TIleGrid() {
|
||||||
|
// Offset height of navbar from window height
|
||||||
|
const height = window.innerHeight - 75;
|
||||||
|
// Offset width of layout padding from window width
|
||||||
|
const width = window.innerWidth;
|
||||||
|
|
||||||
|
const draw = useCallback((g: PixiGraphics) => {
|
||||||
|
g.clear();
|
||||||
|
// Draw dot in the corner of each tile
|
||||||
|
for (let x = 0; x < width; x += 32) {
|
||||||
|
for (let y = 0; y < height; y += 32) {
|
||||||
|
g.beginFill(0xffffff, 0.5);
|
||||||
|
g.drawCircle(x, y, 1);
|
||||||
|
g.endFill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stage
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
options={{
|
||||||
|
backgroundColor: 0x333333,
|
||||||
|
antialias: false
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Graphics draw={draw} />
|
||||||
|
</Stage>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
ui/src/components/TileGrid/tileGrid.css
Normal file
9
ui/src/components/TileGrid/tileGrid.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.tile {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ES2022",
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"ES2022"
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "ES2022",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "Node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
@@ -30,7 +30,8 @@
|
|||||||
"@api/*": ["src/api"],
|
"@api/*": ["src/api"],
|
||||||
"@app/*": ["./src/app/*"],
|
"@app/*": ["./src/app/*"],
|
||||||
"@components/*": ["src/components/*"],
|
"@components/*": ["src/components/*"],
|
||||||
"@lib/*": ["src/components/*"]
|
"@js/*": ["src/js/*"],
|
||||||
|
"@state/*": ["src/state/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
Reference in New Issue
Block a user