frontend: new api client, router guards, better structure

This commit is contained in:
L-Nafaryus 2024-04-16 22:33:34 +05:00
parent 97c9528112
commit 54c97314f8
Signed by: L-Nafaryus
GPG Key ID: 582F8B0866B294A1
57 changed files with 724 additions and 386 deletions

View File

@ -1,7 +1,8 @@
[package]
name = "frontend"
name = "elnafo-frontend"
version = "0.1.0"
edition = "2021"
authors = ["L-Nafaryus <l.nafaryus@elnafo.ru>"]
[build-dependencies]
ignore = "0.4.22"

View File

@ -2,11 +2,11 @@
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/resources/assets/logo.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Elnafo Dev</title>
</head>
<body class="h-full bg-zinc-900 text-zinc-200 font-sans overflow-hidden">
<body class="h-full text-zinc-200 font-sans ">
<div id="app" class="flex flex-col h-full"></div>
<script type="module" src="src/main.ts"></script>
</body>

View File

@ -0,0 +1,19 @@
import axios, { type AxiosInstance } from "axios";
const api_client: AxiosInstance = axios.create({
baseURL: import.meta.hot ? "http://localhost:54600/api" : "/api",
headers: {
"Content-Type": "application/json"
},
withCredentials: true,
});
export const api_client_upload: AxiosInstance = axios.create({
baseURL: import.meta.hot ? "http://localhost:54600/api" : "/api",
headers: {
"Content-Type": "multipart/form-data"
},
withCredentials: true,
});
export default api_client;

View File

@ -0,0 +1,47 @@
import axios, { type AxiosInstance, AxiosError } from "axios";
export class HttpError extends Error {
status_code: number;
constructor(status_code: number, message: string) {
super(JSON.stringify({ status_code: status_code, message: message }));
Object.setPrototypeOf(this, new.target.prototype);
this.name = Error.name;
this.status_code = status_code;
}
}
export interface ResponseError {
status_code: number,
message: string
}
export function handle_error(error: AxiosError): Promise<ResponseError> {
return Promise.reject<ResponseError>({ status_code: error.response?.status, message: error.response?.data });
}
const debug = import.meta.hot;
export const client: AxiosInstance = axios.create({
baseURL: debug ? "http://localhost:54600/api" : "/api",
headers: {
"Content-Type": "application/json"
},
withCredentials: true,
});
export const upload_client: AxiosInstance = axios.create({
baseURL: debug ? "http://localhost:54600/api" : "/api",
headers: {
"Content-Type": "multipart/form-data"
},
withCredentials: true,
});
export const resources_client: AxiosInstance = axios.create({
baseURL: debug ? "http://localhost:54600/resources" : "/resources",
responseType: "blob"
});
export default client;

View File

@ -0,0 +1 @@
export * as user from "@/api/user";

View File

@ -0,0 +1,91 @@
import { client, upload_client, resources_client, type ResponseError, handle_error } from "@/api/client";
export interface NewUser {
login: string,
password: string,
email: string
}
export interface User {
id: string,
login: string,
name: string,
email: string,
is_admin: boolean,
avatar: string
}
export interface RemoveUser {
id: string,
}
export interface LoginUser {
email: string | null,
login: string | null,
password: string,
}
export type Image = string | ArrayBuffer;
export async function register(body: NewUser): Promise<User | ResponseError> {
return await client.post("/user/register", JSON.stringify(body))
.then(async response => { return Promise.resolve<User>(response.data); })
.catch(handle_error);
}
export async function login(body: LoginUser): Promise<User | ResponseError> {
return await client.post("/user/login", JSON.stringify(body))
.then(async response => { return Promise.resolve<User>(response.data); })
.catch(handle_error);
}
export async function remove(body: RemoveUser): Promise<null | ResponseError> {
return await client.post("/user/remove", JSON.stringify(body))
.then(async () => { return Promise.resolve(null); })
.catch(handle_error);
}
export async function logout(): Promise<null | ResponseError> {
return await client.get("/user/logout")
.then(async () => { return Promise.resolve(null); })
.catch(handle_error);
}
export async function current(): Promise<User | ResponseError> {
return await client.get("/user/current")
.then(async response => { return Promise.resolve<User>(response.data); })
.catch(handle_error);
}
export async function avatar(file: FormData, progress?: any): Promise<null | ResponseError> {
return await upload_client.post("/user/avatar", file, {
onUploadProgress: progress ?? null,
//headers: { "Accept-Encoding": "gzip" }
})
.then(async () => { return Promise.resolve(null); })
.catch(handle_error);
}
export async function get_avatar(avatar: string): Promise<Image | null | ResponseError> {
return await resources_client.get("/avatars/".concat(avatar))
.then(async response => {
return new Promise<Image | null>((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = (e) => {
reject(e);
};
reader.readAsDataURL(response.data);
})
})
.catch(handle_error);
}
export async function profile(login: string): Promise<User | ResponseError> {
return await client.get("/user/".concat(login))
.then(async response => { return Promise.resolve<User>(response.data); })
.catch(handle_error);
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1920"
height="634.77374"
viewBox="0 0 508 167.95055"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1"><linearGradient
id="linearGradient1"><stop
style="stop-color:#611e50;stop-opacity:1;"
offset="0.12012421"
id="stop1" /><stop
style="stop-color:#bb418f;stop-opacity:1;"
offset="0.47396368"
id="stop3" /><stop
style="stop-color:#240e54;stop-opacity:1;"
offset="0.85340858"
id="stop2" /></linearGradient><linearGradient
xlink:href="#linearGradient1"
id="linearGradient2"
x1="255.01831"
y1="118.09122"
x2="255.01831"
y2="284.08893"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.99919982,0,0,1,0.44246318,-0.44281006)" /></defs><g
id="layer1"
transform="translate(0,-117.79945)"><g
id="g5"
transform="matrix(0.99992922,0,0,1,-5.5491888e-6,0)"><rect
style="display:inline;fill:url(#linearGradient2);fill-opacity:1;stroke:none;stroke-width:0.999599;stroke-linecap:round;stroke-linejoin:round"
id="rect1"
width="508.03595"
height="167.95055"
x="5.5495816e-06"
y="117.79945" /><path
id="rect5"
d="M 5.5495816e-6,117.79945 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58873 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58871 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58873 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58871 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58873 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58871 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58873 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58871 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 V 227.832 H 5.5495816e-6 Z m 0,3.21892 H 508.03596 v 0.58873 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58873 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58871 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58871 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58872 H 5.5495816e-6 Z m 0,3.21893 H 508.03596 v 0.58873 H 5.5495816e-6 Z m 0,3.21894 H 508.03596 v 0.58872 H 5.5495816e-6 Z"
style="display:inline;opacity:1;fill:#240e54;fill-opacity:0.103402;stroke:none;stroke-width:0.99708;stroke-linecap:round;stroke-linejoin:round" /><path
style="display:inline;fill:#240e54;fill-opacity:1;stroke:none;stroke-width:0.885556;stroke-linecap:round;stroke-linejoin:round"
d="M 5.5495816e-6,250.62991 30.228822,214.19281 l 17.083513,15.19523 33.339423,-26.24165 19.405622,37.11683 7.48651,-10.8368 26.83663,25.33181 35.17775,-49.82103 24.884,14.15112 45.35511,-34.04518 42.08355,37.58458 21.4171,-12.31112 47.04305,30.59522 22.34419,-22.53379 32.93469,32.86757 35.02788,-35.82237 23.74802,31.0568 24.48489,-55.32014 19.15521,59.48051 V 285.75 H 5.5495816e-6 Z"
id="path1" /><path
style="display:inline;fill:#240e54;fill-opacity:0.51184;stroke:none;stroke-width:0.957451;stroke-linecap:round;stroke-linejoin:round"
d="m 508.03596,232.65634 -30.22201,-30.57297 -17.07967,17.76668 -27.96698,-19.40601 -24.76618,32.12157 -7.48483,-12.67069 -26.83059,29.61865 -34.4276,-45.91118 -26.15719,23.27301 -55.34111,-30.48484 -27.02813,37.84322 -32.36862,-28.34042 -40.58932,27.43061 L 135.43458,206.97684 102.5073,245.40651 60.746545,207.79479 33.839975,235.43255 19.537883,197.14664 5.5495816e-6,232.65634 V 285.75 H 508.03596 Z"
id="path3" /></g></g></svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="48" height="48" viewBox="0 0 140.29886 97.999847" version="1.1" id="svg1" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs1" />
<g id="layer1" transform="translate(150.42321,20.813087)">
<g id="g58" transform="matrix(1.8454162,0,0,1.8454162,-242.5425,-75.057662)">
<g id="g45" transform="translate(-17.037401,-18.956659)">
<path
style="fill:#caa65b;fill-opacity:1;stroke:none;stroke-width:0.499602;stroke-dasharray:none;stroke-opacity:1"
d="m 149.89789,64.676766 c -0.6765,-1.565544 -1.35299,-3.499358 -3.00045,-4.762659 -1.64747,-1.263301 -4.26572,-3.148925 -6.37046,-4.299347 -2.10475,-1.150421 -3.69573,-2.490161 -5.36394,-3.66554 -1.66821,-1.17538 -3.41343,-2.186257 -6.16428,-3.13209 -2.75085,-0.945833 -6.50701,-1.82651 -10.67447,-2.24453 -4.16746,-0.418019 -8.74583,-0.373339 -13.32429,-0.328649 -4.57846,-0.04469 -9.156826,-0.08937 -13.324289,0.328649 -4.167463,0.41802 -7.923617,1.298697 -10.67447,2.24453 -2.750853,0.945833 -4.496076,1.95671 -6.164282,3.13209 -1.668206,1.175379 -3.259191,2.515119 -5.363936,3.66554 -2.104745,1.150422 -4.722995,3.036046 -6.370459,4.299347 -1.647464,1.263301 -2.323955,3.197115 -3.000459,4.762659 l 6.856918,3.293368 h 76.100127 z"
id="path23" transform="matrix(0.82368967,0,0,0.91193657,18.512585,6.5794302)" />
<path id="path33" style="fill:#f6eaca;fill-opacity:1;stroke:none;stroke-width:0.499999"
d="m 89.645271,100.95535 c 7.677364,0 23.032089,0 30.709449,0 7.67737,0 7.67737,0 7.67737,0 2.85182,-5.673356 5.12763,-10.200825 6.76769,-16.689792 1.64005,-6.488966 1.54198,-10.577877 1.41909,-15.701677 0,0 0,0 -0.92288,-1.756611 -0.92287,-1.75661 -2.76862,-5.269827 -7.82717,-7.506991 -5.05854,-2.237164 -10.2366,-2.165312 -14.51039,-1.1889 -4.27378,0.976412 -6.1161,2.780155 -7.95843,2.798806 -1.84233,0.01865 -3.68465,-1.747787 -7.958428,-2.724197 -4.273778,-0.97641 -10.650225,-1.101805 -14.922532,0.768792 -4.272307,1.870598 -6.305108,5.739849 -8.337908,9.609101 0,0 0,0 0,0 -0.07678,5.1238 -0.138053,9.212711 1.502,15.701677 1.640053,6.488965 3.879072,11.016436 6.684778,16.689792 0,0 -2e-6,0 7.677361,0 z" />
<path
style="fill:#764d3c;fill-opacity:1;stroke:#260d02;stroke-width:1.15381;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 67.35977,76.543165 c -2.414103,-0.851341 -5.148126,-1.70268 -6.611426,-4.165251 -1.463299,-2.462571 -1.855697,-5.243411 -0.646239,-7.701148 1.209458,-2.457738 4.072368,-3.145027 7.00378,-3.986704 z m 75.36019,0 c 2.4141,-0.851341 4.8282,-1.70268 6.2915,-4.165251 1.4633,-2.462571 2.09589,-5.243411 0.88643,-7.701148 -1.20945,-2.457738 -4.01268,-3.145027 -6.9441,-3.986704 z"
id="path21" transform="matrix(0.82368967,0,0,0.91193657,18.512585,6.5794302)" />
<path
style="fill:#613723;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 73.89157,69.153428 -0.10456,-7.228512 c -2.192267,0.905974 -4.384535,1.811949 -5.365122,3.343097 -0.980587,1.531148 -0.749495,3.68747 -0.518402,5.843792 0.36662,-0.673067 0.733241,-1.346133 1.372028,-1.854518 0.638788,-0.508384 1.549742,-0.852087 2.364446,-0.841949 0.814704,0.01014 1.533157,0.374113 2.25161,0.73809 z m 62.21686,0 0.10456,-7.228512 c 2.19227,0.905974 4.38454,1.811949 5.36512,3.343097 0.98059,1.531148 0.7495,3.68747 0.5184,5.843792 -0.36662,-0.673067 -0.73324,-1.346133 -1.37202,-1.854518 -0.63879,-0.508384 -1.54975,-0.852087 -2.36445,-0.841949 -0.8147,0.01014 -1.53316,0.374113 -2.25161,0.73809 z"
id="path54" />
<path
style="fill:#e2ce9a;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 99.414974,77.197367 C 101.27665,74.319538 103.13829,71.441764 105,71.441763 c 1.86171,0 3.72335,2.877775 5.58503,5.755604 z"
id="path51" />
<path id="path52"
style="fill:#d7b774;fill-opacity:1;stroke-width:2.03025;stroke-linecap:round;stroke-linejoin:round"
d="m 110.58503,77.197369 c 0,0.899566 -0.66849,3.330336 -1.70087,4.783905 -1.03237,1.453569 -2.42863,1.929938 -3.88416,1.929938 -1.45553,0 -2.85179,-0.476369 -3.88416,-1.929938 -1.03238,-1.45357 -1.70087,-3.884339 -1.70087,-4.783905 0,-1.799132 2.5005,-1.562812 5.58503,-1.562812 3.08453,0 5.58503,-0.23632 5.58503,1.562812 z" />
<path id="path27" style="fill:#260d02;fill-opacity:1;stroke:none;stroke-width:0.499999"
d="m 110.58503,77.197369 c 0,1.542263 -2.5005,3.209678 -5.58503,3.209677 -3.08452,-10e-7 -5.585025,-1.667416 -5.585025,-3.209677 0,-1.542261 2.500505,-2.375348 5.585025,-2.375349 3.08453,-10e-7 5.58503,0.833086 5.58503,2.375349 z" />
<path
style="fill:#764d3c;fill-opacity:1;stroke:none;stroke-width:0.499999;stroke-dasharray:none;stroke-opacity:1"
d="m 112.7133,67.416323 c 0.0638,2.364566 1.27095,5.236409 3.24083,6.787235 2.08535,1.641729 5.16848,2.064956 7.78963,1.648409 3.35389,-0.532993 7.76222,-2.193587 8.8223,-5.095203 1.06007,-2.901617 -1.16598,-7.671931 -4.64597,-8.875533 -3.47998,-1.203602 -8.2135,-1.131007 -11.16124,-0.03745 -2.94774,1.093556 -4.10939,3.207976 -4.04555,5.572542 z m -15.4266,0 c -0.06384,2.364566 -1.353158,4.979171 -3.240829,6.787235 -1.88767,1.808065 -4.37348,2.80942 -7.78963,1.648409 -3.41615,-1.16101 -7.762222,-2.193587 -8.822297,-5.095203 -1.060075,-2.901617 1.165977,-7.671931 4.645961,-8.875533 3.479985,-1.203602 8.213505,-1.131007 11.161247,-0.03745 2.947743,1.093556 4.109387,3.207976 4.045548,5.572542 z"
id="path28" />
<path
style="fill:#260d02;fill-opacity:1;stroke:none;stroke-width:0.589188;stroke-dasharray:none;stroke-opacity:1"
d="m 114.94392,69.780609 c 0,2.655366 1.87253,4.042592 4.52791,4.042592 2.65536,0 5.08803,-1.387226 5.08803,-4.042592 0,-2.655368 -2.1526,-4.807967 -4.80797,-4.807967 -2.65537,0 -4.80797,2.152599 -4.80797,4.807967 z"
id="path53" />
<path
style="fill:#260d02;fill-opacity:1;stroke:none;stroke-width:0.578499;stroke-dasharray:none;stroke-opacity:1"
d="m 94.968852,69.722679 c 0,2.607195 -2.113549,4.720744 -4.720744,4.720744 -2.607195,0 -4.720744,-2.113549 -4.720744,-4.720744 0,-2.607195 2.113549,-4.720744 4.720744,-4.720744 2.607195,0 4.720744,2.113549 4.720744,4.720744 z"
id="path29" />
<path style="fill:none;stroke:#260d02;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
d="m 105.65276,83.392427 c 0,1.174279 0,2.348558 0,3.522837" id="path15"
transform="translate(-0.65276,-3.0040522)" />
<path
style="fill:none;stroke:#260d02;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 94.045889,87.659756 c 2.556754,0.59495 3.956812,0.47748 5.921244,0.266356 1.725607,-0.185456 3.487697,-0.595661 5.032867,-1.33388 1.54517,0.738219 3.09034,1.476437 5.03287,1.33388 1.94252,-0.142556 5.38536,-0.936201 6.62226,-3.822913"
id="path31" transform="translate(0,-2.6810205)" />
<path id="path45" transform="translate(0,-0.58955077)"
d="m 92.829198,68.313416 c 0,0.463928 -0.376088,0.840016 -0.840016,0.840016 -0.463929,0 -0.840017,-0.376088 -0.840017,-0.840016 0,-0.463929 0.376088,-0.840017 0.840017,-0.840017 0.463928,0 0.840016,0.376088 0.840016,0.840017 z m 24.341602,0 c 0,0.463928 0.37609,0.840016 0.84002,0.840016 0.46393,0 0.84001,-0.376088 0.84001,-0.840016 0,-0.463929 -0.37608,-0.840017 -0.84001,-0.840017 -0.46393,0 -0.84002,0.376088 -0.84002,0.840017 z"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.433001;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:#e2ce9a;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 80.264904,54.03489 c 3.14633,-1.696538 6.29266,-3.393076 10.415177,-4.273728 4.122516,-0.880652 9.221217,-0.945417 14.368409,-0.92669 5.14719,0.01873 10.34287,0.120947 14.4837,0.988087 4.14083,0.86714 7.22681,2.499202 10.31279,4.131263 -4.13167,-1.127737 -8.26335,-2.255476 -12.39502,-2.549174 -4.13167,-0.293699 -8.26334,0.246639 -12.39502,0.253395 -4.13168,0.0068 -8.263344,-0.520071 -12.395017,-0.212861 -4.131674,0.307211 -8.263346,1.448459 -12.395019,2.589708 z"
id="path56" />
<path
style="fill:none;fill-opacity:1;stroke:#260d02;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 68.018066,65.560537 c 0.823817,-1.447748 1.647633,-2.895493 2.934106,-4.272836 1.286473,-1.377343 3.035529,-2.684226 4.646461,-3.894816 1.610931,-1.210591 3.083641,-2.324816 4.666271,-3.357993 1.582629,-1.033178 3.27508,-1.985248 5.586761,-2.802444 2.311681,-0.817197 5.242439,-1.499472 8.537063,-1.890569 3.294625,-0.391097 6.952912,-0.490998 10.611302,-0.490997 3.6584,1e-6 7.31669,0.0999 10.61131,0.491005 3.29462,0.391103 6.22537,1.073378 8.53704,1.890577 2.31167,0.8172 4.00412,1.769271 5.58675,2.802448 1.58262,1.033178 3.05534,2.147406 4.66627,3.357995 1.61093,1.210588 3.35999,2.517474 4.64646,3.89482 1.28646,1.377345 2.11026,2.82506 2.93407,4.27281"
id="path46" />
<path
style="fill:none;fill-opacity:1;stroke:#260d02;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 73.996131,76.38194 c 0.429,3.8539 0.857991,7.707726 2.186647,11.803376 1.328656,4.09565 3.55687,8.432797 5.78513,12.770034"
id="path47" />
<path
style="fill:none;fill-opacity:1;stroke:#260d02;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 136.06954,76.38194 c -0.39825,4.12443 -0.79649,8.248778 -2.13609,12.344427 -1.3396,4.095649 -3.62046,8.162278 -5.90136,12.228983"
id="path48" />
<ellipse
style="fill:#4d2f24;fill-opacity:1;stroke:none;stroke-width:0.774066;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path49" cx="104.96773" cy="76.749893" rx="3.82424" ry="0.85318637" />
<path
style="fill:none;fill-opacity:1;stroke:#caa65b;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 102.22569,90.834343 c 0,0 1.83164,0.285232 2.77431,0.285232 0.94267,0 2.77431,-0.285232 2.77431,-0.285232"
id="path50" />
<path
style="fill:none;fill-opacity:1;stroke:#260d02;stroke-width:1.15381;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 67.35977,76.543165 c -2.414103,-0.851341 -5.148126,-1.70268 -6.611426,-4.165251 -1.463299,-2.462571 -1.855697,-5.243411 -0.646239,-7.701148 1.209458,-2.457738 4.072368,-3.145027 7.00378,-3.986704 z m 75.36019,0 c 2.4141,-0.851341 4.8282,-1.70268 6.2915,-4.165251 1.4633,-2.462571 2.09589,-5.243411 0.88643,-7.701148 -1.20945,-2.457738 -4.01268,-3.145027 -6.9441,-3.986704 z"
id="path55" transform="matrix(0.82368967,0,0,0.91193657,18.512585,6.5794302)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,50 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
background-color: rgba(36, 14, 84, 1); /*linear-gradient(rgba(36, 14, 84, 1) 80%, rgba(55, 22, 130, 1)); */
background-image: url("./background.svg");
background-position: left top;
background-repeat: repeat-x;
}
a {
@apply text-green-500 hover:text-green-400;
}
h1 {
font-family: BioRhyme,serif;
font-weight: 700;
}
label {
font-family: Space Mono,monospace;
font-weight: 500;
}
}
@layer utilities {
.bg-roll::before {
background: linear-gradient(90deg, #fb0094, #0000ff, #fb0093);
background-size: 200%;
@apply absolute w-[100%] h-[100%] content-[''] animate-border-roll;
}
.bg-grid {
background:
linear-gradient(180deg, rgba(0, 0, 0, 0) 0px, rgba(187, 65, 143, 1) 10%,
rgba(187, 65, 143, 1) 2px, rgba(0, 0, 0, 0) 0px),
linear-gradient(90deg, rgba(0, 0, 0, 0) 0px, rgba(187, 65, 143, 1) 10%,
rgba(187, 65, 143, 1) 2px, rgba(0, 0, 0, 0) 0px);
background-size: 2em 4em, 6em 2em;
transform: perspective(500px) rotateX(60deg) scale(0.5);
transform-origin: 50% 0%;
z-index: -1;
@apply absolute w-[250%] -left-[75%] h-[200%];
}
}

View File

@ -2,11 +2,9 @@ use askama_axum::Template;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "dist/assets/"]
#[folder = "dist/resources/assets/"]
pub struct Assets;
// TODO: parse assets and add paths to templates
#[derive(Template)]
#[template(path = "base.html")]
pub struct BaseTemplate<'a> {

View File

@ -0,0 +1,86 @@
import { createRouter, createWebHistory } from "vue-router";
import { user } from "@/api";
import { useUserStore } from "@/stores";
async function check_authorized(): Promise<boolean> {
const userStore = useUserStore();
// TODO: add timer
return await user.current()
.then(async user => { userStore.current = user; })
.then(async () => {
if (userStore.current.avatar?.length) {
await user.get_avatar(userStore.current.avatar)
.then(async avatar => { userStore.avatar = avatar; })
}
})
.then(async () => { return true; })
.catch(() => {
return false;
});
}
async function bypass_auth(to: any, from: any) {
if (await check_authorized() && (to.name === "signin" || to.name === "signup")) {
return from;
}
}
async function required_auth(to: any, from: any) {
if (!await check_authorized()) {
return { name: "signin" };
}
}
async function required_admin(to: any, from: any) {
const userStore = useUserStore();
return userStore.current.is_admin;
}
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: "/", name: "home", beforeEnter: [bypass_auth],
component: () => import("@/views/Home.vue"),
},
{
path: "/user/login", name: "signin", beforeEnter: [bypass_auth],
component: () => import("@/views/user/SignIn.vue")
},
{
path: "/user/register", name: "signup", //beforeEnter: [bypass_auth],
component: () => import("@/views/user/SignUp.vue")
},
{
path: "/user/preferencies", name: "prefs", redirect: { name: "prefs-profile" }, beforeEnter: [required_auth],
component: () => import("@/views/user/Preferencies.vue"),
children: [
{
path: "profile", name: "prefs-profile", beforeEnter: [required_auth],
component: () => import("@/views/user/preferencies/Profile.vue")
},
{
path: "account", name: "prefs-account", beforeEnter: [required_auth],
component: () => import("@/views/user/preferencies/Account.vue")
},
]
},
{
path: "/:user", name: "profile", beforeEnter: [bypass_auth],
component: () => import("@/views/user/Profile.vue")
},
{
path: "/admin/settings", name: "settings", beforeEnter: [required_auth, required_admin],
component: () => import("@/views/admin/Settings.vue")
},
{
path: "/:pathMatch(.*)*", name: "not-found", beforeEnter: [bypass_auth],
component: () => import("@/views/error/NotFound.vue")
}
]
});
export default router;

View File

@ -0,0 +1,23 @@
import { defineStore } from "pinia";
import { ref, type Ref } from "vue";
import { user } from "@/api";
export const useUserStore = defineStore("user", () => {
const current: Ref<user.User | null> = ref(null);
const avatar: Ref<Blob | null> = ref(null);
function clear() {
current.value = null;
avatar.value = null;
}
return { current, avatar, clear };
});
export const useMiscStore = defineStore("misc", () => {
// preferencies current tab
const p_current_tab: Ref<number> = ref(0);
return { p_current_tab };
});

View File

@ -6,43 +6,19 @@ import DropdownMenu from "@/components/DropdownMenu.vue";
import { ref, onMounted } from "vue";
import router from "@/router";
import User from "@/services/user";
import { useUserStore } from "@/stores/user";
import { user } from "@/api";
import { useUserStore } from "@/stores";
const user = ref(null);
const userStore = useUserStore();
const error = ref<string>(null);
const error = ref(null);
onMounted(async () => {
await User.current()
.then(async response => {
if (response.status != 200) {
return Promise.reject(response.data && response.data.message || response.status);
};
if (response.data.hasOwnProperty("user")) {
userStore.login = response.data.user.login;
};
})
.catch(e => {
console.log(`${e.name}[${e.code}]: ${e.message}`);
});
});
async function user_logout() {
await User.logout()
.then(async response => {
error.value = null;
if (response.status != 200) {
return Promise.reject(response.data && response.data.message || response.status);
};
userStore.login = null;
async function signout() {
await user.logout()
.then(async () => {
userStore.clear();
router.push({ path: "/" });
})
.catch(e => {
console.error("Error occured:", e);
});
.catch(error => { error.value = error; });
}
</script>
@ -54,50 +30,54 @@ async function user_logout() {
<Meerkat />
</template>
<template #right>
<DropdownMenu v-if="userStore.login">
<DropdownMenu v-if="userStore.current">
<template #button>
<span
class="flex min-w-9 min-h-9 pt-1 pb-1 pl-3 pr-3 rounded hover:bg-zinc-600 cursor-pointer">{{
userStore.login }}</span>
<div class="pl-3 pr-3 flex gap-2 items-center rounded hover:bg-zinc-600 cursor-pointer">
<div class="max-w-8" v-if="userStore.avatar"><img :src="userStore.avatar"></div>
<span class="flex min-w-9 min-h-9 items-center">{{
userStore.current.login }}</span>
</div>
</template>
<template #content>
<div
class="absolute flex flex-col left-auto right-0 mt-4 bg-zinc-700 border rounded border-zinc-500 mr-3">
<RouterLink :to="{ name: 'Profile', params: { user: userStore.login } }"
class="absolute z-20 flex flex-col left-auto right-0 mt-4 bg-zinc-700 border rounded border-zinc-500 mr-3">
<RouterLink :to="{ name: 'profile', params: { user: userStore.current.login } }"
class="flex min-w-7 pl-5 pr-5 pt-1 pb-1 hover:bg-zinc-600">
Profile</RouterLink>
<RouterLink :to="{ name: 'Preferencies' }"
<RouterLink :to="{ name: 'prefs' }"
class="flex min-w-7 pl-5 pr-5 pt-1 pb-1 hover:bg-zinc-600">
Preferencies</RouterLink>
<div class="border-t border-zinc-500 ml-0 mr-0"></div>
<RouterLink :to="{ name: 'Settings' }"
<RouterLink v-if="userStore.current.is_admin" :to="{ name: 'settings' }"
class="flex min-w-7 pl-5 pr-5 pt-1 pb-1 hover:bg-zinc-600">
Settings</RouterLink>
<div class="border-t border-zinc-500 ml-0 mr-0"></div>
<div @click="user_logout"
<div @click="signout"
class="flex min-w-7 pl-5 pr-5 pt-1 pb-1 hover:bg-zinc-600 cursor-pointer">
Sign Out</div>
</div>
</template>
</DropdownMenu>
<RouterLink v-if="!userStore.login"
<RouterLink v-if="!userStore.current"
class="flex min-w-9 min-h-9 pt-1 pb-1 pl-3 pr-3 rounded hover:bg-zinc-600" to="/user/login">
Sign In</RouterLink>
</template>
</NavBar>
<main class="overflow-hidden">
<div>
<div class="bg-grid"></div>
</div>
<main>
<slot></slot>
</main>
</div>
<div class="relative overflow-hidden h-full ">
</div>
<footer
class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-gradient-to-b from-zinc-800 to-zinc-900 border-t border-t-zinc-500">
<a href="/">Made with glove</a>
<a href="/api/v1">API</a>
</footer>
</template>
<style></style>

View File

@ -3,9 +3,9 @@ import Base from "@/views/Base.vue";
import { ref, onMounted, watch, getCurrentInstance } from "vue";
import { usePreferenciesStore } from "@/stores/preferencies.ts";
import { useMiscStore } from "@/stores";
const preferenciesStore = usePreferenciesStore();
const miscStore = useMiscStore();
</script>
@ -16,12 +16,10 @@ const preferenciesStore = usePreferenciesStore();
<div>
<div class="border rounded border-zinc-500 flex-col w-64 side-nav">
<h1 class="pl-5 pr-5 pt-2 pb-2">User Preferencies</h1>
<RouterLink :to="{ name: 'Preferencies-Profile' }"
:class="{ 'bg-zinc-600': preferenciesStore.current_tab === 0 }"
<RouterLink :to="{ name: 'prefs-profile' }" :class="{ 'bg-zinc-600': miscStore.p_current_tab === 0 }"
class="flex min-w-7 pl-5 pr-5 pt-2 pb-2 hover:bg-zinc-600 border-t border-zinc-500">
Profile</RouterLink>
<RouterLink :to="{ name: 'Preferencies-Account' }"
:class="{ 'bg-zinc-600': preferenciesStore.current_tab === 1 }"
<RouterLink :to="{ name: 'prefs-account' }" :class="{ 'bg-zinc-600': miscStore.p_current_tab === 1 }"
class="flex min-w-7 pl-5 pr-5 pt-2 pb-2 hover:bg-zinc-600 border-t border-zinc-500">
Account</RouterLink>
</div>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/error/Error.vue";
import { ref, onMounted, watch, getCurrentInstance } from "vue";
import { onBeforeRouteUpdate, useRoute } from "vue-router"
import { user } from "@/api";
import { useUserStore } from "@/stores";
const route = useRoute();
const userStore = useUserStore();
const error = ref<string>(null);
const person = ref<user.User>(null);
const avatar = ref<user.Image>(null);
async function profile(login: string) {
await user.profile(login)
.then(async user => { person.value = user; })
.then(async () => {
if (person.value.avatar?.length) {
await user.get_avatar(person.value.avatar)
.then(async _avatar => { avatar.value = _avatar; })
}
})
.catch(error => { error.value = error; });
};
onMounted(async () => {
await profile(route.params.user);
});
watch(route, async (to, from) => {
await profile(to.params.user);
});
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<Error v-if="error">{{ error }}</Error>
<p v-if="person">{{ person.name }}</p>
<div class="max-w-8" v-if="avatar"><img :src="avatar"></div>
</div>
</Base>
</template>

View File

@ -2,43 +2,54 @@
import Base from "@/views/Base.vue";
import Error from "@/components/error/Error.vue";
import { ref } from "vue";
import { ref, onMounted } from "vue";
import router from "@/router";
import User from "@/services/user";
import { useUserStore } from "@/stores/user";
import { user } from "@/api";
import { useUserStore } from "@/stores";
const email = defineModel("email");
const email_or_login = defineModel("email_or_login");
const password = defineModel("password");
const error = ref(null);
const userStore = useUserStore();
const error = ref(null);
async function login() {
await User.login(email.value, password.value)
.then(async response => {
if (response.status != 200) {
return Promise.reject(response.data && response.data.message || response.status);
onMounted(async () => {
if (userStore.current) {
router.replace({ path: "/" });
}
});
async function signin() {
const body: user.LoginUser = {
email: null,
login: null,
password: password.value
};
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email_or_login.value)) {
body.email = email_or_login.value;
} else {
body.login = email_or_login.value;
}
userStore.login = response.data.user.login;
router.push({ path: `/${userStore.login}` });
await user.login(body)
.then(async user => {
userStore.current = user;
router.push({ path: "/" });
})
.catch(e => {
error.value = e;
console.log(`${e.name}[${e.code}]: ${e.message}`);
});
.catch(error => { error.value = error; });
};
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<h4 class="text-center pt-5 pb-5 border-b border-zinc-500">Sign In</h4>
<h1 class="text-center pt-5 pb-5 border-b border-zinc-500">Sign In</h1>
<form @submit.prevent class="m-auto pt-5 pb-5">
<div class="mb-5 ml-auto mr-auto">
<label for="email" class="text-right w-64 inline-block mr-5">Email Address</label>
<input v-model="email" type="email" placeholder="" name="email" required
<label for="email_or_login" class="text-right w-64 inline-block mr-5">Email or Login</label>
<input v-model="email_or_login" placeholder="" name="email_or_login" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
@ -49,7 +60,7 @@ async function login() {
<div class="mb-5 ml-auto mr-auto">
<label class="text-right w-64 inline-block mr-5"></label>
<div class="flex justify-between items-center w-1/2 m-auto">
<button @click="login" class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
<button @click="signin" class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
In</button>
<p>or</p>
<button @click="$router.push('/user/register')"

View File

@ -5,7 +5,7 @@ import Error from "@/components/error/Error.vue";
import { ref } from "vue";
import router from "@/router";
import User from "@/services/user";
import { user } from "@/api";
const login = defineModel("login");
const email = defineModel("email");
@ -13,19 +13,10 @@ const password = defineModel("password");
const error = ref(null);
async function register() {
await User.register(login.value, email.value, password.value)
.then(async response => {
if (response.status != 200) {
return Promise.reject(response.data && response.data.message || response.status);
}
router.push({ path: "/user/login" });
})
.catch(e => {
error.value = e;
console.log(`${e.name}[${e.code}]: ${e.message}`);
});
async function signup() {
await user.register({ login: login.value, password: password.value, email: email.value })
.then(async user => { router.push({ path: "/user/login" }); })
.catch(error => { error.value = error; });
};
</script>
@ -51,11 +42,11 @@ async function register() {
</div>
<div class="mb-5 ml-auto mr-auto">
<label class="text-right w-64 inline-block mr-5"></label>
<button @click="register" class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
<button @click="signup" class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
Up</button>
</div>
</form>
<Error v-if="error">{{ error }}</Error>
<Error v-if="error">{{ error.message }}</Error>
</div>
</Base>
</template>

View File

@ -4,9 +4,7 @@ import Base from "@/views/Base.vue";
import { ref, onMounted, watch, getCurrentInstance } from "vue";
import router from "@/router";
import User from "@/services/user";
import { useUserStore } from "@/stores/user";
import { usePreferenciesStore } from "@/stores/preferencies.ts";
import { useUserStore, useMiscStore } from "@/stores";
const password = defineModel("password");
const new_password = defineModel("new-password");
@ -19,31 +17,17 @@ const confirm_password = defineModel("confirm-password");
const error = ref(null);
const userStore = useUserStore();
const preferenciesStore = usePreferenciesStore();
const miscStore = useMiscStore();
onMounted(async () => {
preferenciesStore.current_tab = 1;
miscStore.p_current_tab = 1;
!userStore.login ? router.push({ name: "SignIn" }) : await User.current()
.then(async response => {
error.value = null;
if (response.status != 200) {
return Promise.reject(response.data && response.data.message || response.status);
};
email.value = response.data.user.email;
})
.catch(e => {
error.value = e;
console.log(`${e.name}[${e.code}]: ${e.message}`);
});
});
</script>
<template>
<div class="flex flex-col gap-4 ml-auto mr-auto w-full">
<div class="border rounded border-zinc-500 w-full flex-col">
<div class="border rounded border-zinc-500 w-full flex-col bg-zinc-800 bg-opacity-95">
<h1 class="pl-5 pr-5 pt-2 pb-2">Password</h1>
<div class="border-t border-zinc-500 p-5">
<form @submit.prevent class="">
@ -69,7 +53,7 @@ onMounted(async () => {
</div>
</div>
<div class="border rounded border-zinc-500 w-full flex-col">
<div class="border rounded border-zinc-500 w-full flex-col bg-zinc-800 bg-opacity-95">
<h1 class="pl-5 pr-5 pt-2 pb-2">Email</h1>
<div class="border-t border-zinc-500 p-5">
<form @submit.prevent class="">
@ -89,7 +73,7 @@ onMounted(async () => {
</div>
</div>
<div class="border rounded border-red-500 w-full flex-col">
<div class="border rounded border-red-500 w-full flex-col bg-zinc-800 bg-opacity-95">
<h1 class="pl-5 pr-5 pt-2 pb-2">Delete account</h1>
<div class="border-t border-red-500 p-5">
<form @submit.prevent class="">

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import { ref, onMounted, watch, getCurrentInstance } from "vue";
import router from "@/router";
import { user } from "@/api";
import { useUserStore, useMiscStore } from "@/stores";
const error = ref(null);
const userStore = useUserStore();
const miscStore = useMiscStore();
const login = defineModel("login");
const name = defineModel("name");
const email = defineModel("email");
const image_file = ref(null);
const progress = ref(0);
const avatar_preview = ref(null);
onMounted(async () => {
miscStore.p_current_tab = 0;
login.value = userStore.current.login;
});
function uploadFile(event) {
image_file.value = event.target.files.item(0);
avatar_preview.value = URL.createObjectURL(image_file.value);
progress.value = 0;
}
async function submitFile() {
await user.avatar(image_file.value, (event) => {
progress.value = Math.round((100 * event.loaded) / event.total);
})
.catch(error => { error.value = error });
}
</script>
<template>
<div class="flex flex-col gap-4 ml-auto mr-auto w-full">
<div class="border rounded border-zinc-500 w-full flex-col">
<h1 class="pl-5 pr-5 pt-2 pb-2">Profile Info</h1>
<div class="border-t border-zinc-500 p-5">
<form @submit.prevent class="">
<div>
<label class="block mb-2" for="login">Login</label>
<input v-model="login" name="login"
class="w-full bg-zinc-800 pl-3 pr-3 pt-2 pb-2 mb-4 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div>
<label class="block mb-2" for="name">Username</label>
<input v-model="name" name="name"
class="w-full bg-zinc-800 pl-3 pr-3 pt-2 pb-2 mb-4 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div>
<label class="block mb-2 " for="email">Email</label>
<input v-model="email" email="email" disabled
class="w-full bg-zinc-800 pl-3 pr-3 pt-2 pb-2 mb-4 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="border-t border-zinc-500 ml-0 mr-0 mt-3 mb-3"></div>
<button
class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5 ml-auto mr-0 block">Update</button>
</form>
</div>
</div>
<div class="border rounded border-zinc-500 w-full flex-col">
<h1 class="pl-5 pr-5 pt-2 pb-2">User avatar</h1>
<div class="border-t border-zinc-500 p-5">
<form @submit.prevent class="" enctype="multipart/form-data">
<div>
<label class="block mb-2 " for="avatar">New avatar</label>
<input name="avatar" type="file" ref="file" accept="image/png,image/jpeg,image/jpg"
@change="uploadFile"
class="w-full bg-zinc-800 pl-3 pr-3 pt-2 pb-2 mb-4 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="flex flex-row gap-8 items-center">
<div class="max-w-64"><img :src="avatar_preview"></div>
<div class="max-w-32"><img :src="avatar_preview"></div>
<div class="max-w-16"><img :src="avatar_preview"></div>
</div>
<div class="border-t border-zinc-500 ml-0 mr-0 mt-3 mb-3"></div>
<button @click="submitFile" :disabled="!image_file"
class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5 ml-auto mr-0 block">Update
avatar</button>
<p>{{ progress }}</p>
</form>
</div>
</div>
</div>
</template>

View File

@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,ts,js}'],
content: ["./index.html", "./src/**/*.{vue,ts,js}"],
theme: {
extend: {
keyframes: {

View File

@ -2,11 +2,11 @@
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/resources/assets/logo.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Elnafo</title>
<script type="module" crossorigin src="/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index.css">
<script type="module" crossorigin src="/resources/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/resources/assets/index.css">
</head>
<body class="h-full bg-zinc-900 text-zinc-200 font-sans">
<div id="{{ view }}" class="flex flex-col h-full"></div>

View File

@ -0,0 +1,20 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": [
"node",
"vite"
]
}
}

View File

@ -0,0 +1,25 @@
import { fileURLToPath, URL } from "node:url"
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import vueJsx from "@vitejs/plugin-vue-jsx"
export default defineConfig({
plugins: [
vue(),
vueJsx(),
],
build: {
rollupOptions: {
output: {
entryFileNames: "resources/assets/[name].js",
assetFileNames: "resources/assets/[name][extname]",
chunkFileNames: "resources/assets/[name].js"
}
}
},
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url))
}
}
})

View File

@ -1,11 +0,0 @@
import axios, { type AxiosInstance } from "axios";
const api_client: AxiosInstance = axios.create({
baseURL: import.meta.hot ? "http://localhost:54600/api/v1" : "/api/v1",
headers: {
"Content-Type": "application/json"
},
withCredentials: true,
});
export default api_client;

View File

@ -1,31 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
a {
@apply text-green-500 hover:text-green-400;
}
}
@layer utilities {
.bg-roll::before {
background: linear-gradient(90deg, #fb0094, #0000ff, #fb0093);
background-size: 200%;
@apply absolute w-[100%] h-[100%] content-[''] animate-border-roll;
}
.bg-grid {
background:
linear-gradient(180deg, rgba(0, 0, 0, 0) 0px, rgba(113, 113, 122, 1) 0%,
rgba(113, 113, 122, 1) 2px, rgba(0, 0, 0, 0) 0px),
linear-gradient(90deg, rgba(0, 0, 0, 0) 0px, rgba(113, 113, 122, 1) 0%,
rgba(113, 113, 122, 1) 2px, rgba(0, 0, 0, 0) 0px);
background-size: 2em 4em, 6em 2em;
transform: perspective(300px) rotateX(30deg) scale(1);
z-index: -1;
@apply absolute w-[200%] -left-[50%] h-full;
}
}

View File

@ -1,25 +0,0 @@
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", name: "Home", component: () => import("@/views/Home.vue") },
{ path: "/user/login", name: "SignIn", component: () => import("@/views/user/SignIn.vue") },
{ path: "/user/register", name: "SignUp", component: () => import("@/views/user/SignUp.vue") },
{
path: "/user/preferencies", name: "Preferencies", redirect: { name: "Preferencies-Profile" }, component: () => import("@/views/user/Preferencies.vue"), children: [
{ path: "profile", name: "Preferencies-Profile", component: () => import("@/views/user/preferencies/Profile.vue") },
{ path: "account", name: "Preferencies-Account", component: () => import("@/views/user/preferencies/Account.vue") },
]
},
{ path: "/:user", name: "Profile", component: () => import("@/views/user/Profile.vue") },
{ path: "/admin/settings", name: "Settings", component: () => import("@/views/admin/Settings.vue") },
{ path: "/:pathMatch(.*)*", name: "NotFound", component: () => import("@/views/error/NotFound.vue") }
]
});
export default router;

View File

@ -1,25 +0,0 @@
import api_client from "@/api-client";
class User {
async register(login: string, email: string, password: string): Promise<JSON> {
return await api_client.post("/user/register", JSON.stringify({ login: login, email: email, password: password }));
}
async login(email: string, password: string): Promise<JSON> {
return await api_client.post("/user/login", JSON.stringify({ email: email, password: password }));
}
async logout(): Promise<JSON> {
return await api_client.get("/user/logout");
}
async get(login: any): Promise<JSON> {
return await api_client.get(`/user/${login}`);
}
async current(): Promise<JSON> {
return await api_client.get("/user/current");
}
}
export default new User();

View File

@ -1,5 +0,0 @@
import { defineStore } from "pinia";
export const usePreferenciesStore = defineStore("preferencies", {
state: () => ({ current_tab: null }),
});

View File

@ -1,5 +0,0 @@
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({ login: null }),
});

View File

@ -1,53 +0,0 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/error/Error.vue";
import { ref, onMounted, watch, getCurrentInstance } from "vue";
import { onBeforeRouteUpdate, useRoute } from "vue-router"
import User from "@/services/user";
import { useUserStore } from "@/stores/user";
const route = useRoute();
const name = ref<string>(null);
const userStore = useUserStore();
const error = ref<string>(null);
async function user_profile(login: string) {
await User.get(login)
.then(async response => {
error.value = null;
if (response.status != 200) {
return Promise.reject(response.data && response.data.message || response.status);
};
if (response.data.hasOwnProperty("user")) {
name.value = response.data.user.name;
} else {
error.value = "404 Not Found";
};
})
.catch(e => {
console.error("Error occured:", e);
});
};
onMounted(async () => {
await user_profile(route.params.user);
});
watch(route, async (to, from) => {
await user_profile(to.params.user);
});
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<Error v-if="error">{{ error }}</Error>
<p v-else>{{ name }}</p>
</div>
</Base>
</template>

View File

@ -1,67 +0,0 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import { ref, onMounted, watch, getCurrentInstance } from "vue";
import router from "@/router";
import User from "@/services/user";
import { useUserStore } from "@/stores/user";
import { usePreferenciesStore } from "@/stores/preferencies.ts";
const login = defineModel("login");
const name = defineModel("name");
const email = defineModel("email");
const error = ref(null);
const userStore = useUserStore();
const preferenciesStore = usePreferenciesStore();
onMounted(async () => {
preferenciesStore.current_tab = 0;
!userStore.login ? router.push({ name: "SignIn" }) : await User.current()
.then(async response => {
error.value = null;
if (response.status != 200) {
return Promise.reject(response.data && response.data.message || response.status);
};
login.value = response.data.user.login;
name.value = response.data.user.name;
email.value = response.data.user.email;
})
.catch(e => {
error.value = e;
console.log(`${e.name}[${e.code}]: ${e.message}`);
})
});
</script>
<template>
<div class="border rounded border-zinc-500 w-full flex-col">
<h1 class="pl-5 pr-5 pt-2 pb-2">Profile Info</h1>
<div class="border-t border-zinc-500 p-5">
<form @submit.prevent class="">
<div>
<label class="block mb-2" for="login">Login</label>
<input v-model="login" name="login"
class="w-full bg-zinc-800 pl-3 pr-3 pt-2 pb-2 mb-4 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div>
<label class="block mb-2" for="name">Username</label>
<input v-model="name" name="name"
class="w-full bg-zinc-800 pl-3 pr-3 pt-2 pb-2 mb-4 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div>
<label class="block mb-2 " for="email">Email</label>
<input v-model="email" email="email" disabled
class="w-full bg-zinc-800 pl-3 pr-3 pt-2 pb-2 mb-4 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="border-t border-zinc-500 ml-0 mr-0 mt-3 mb-3"></div>
<button
class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5 ml-auto mr-0 block">Update</button>
</form>
</div>
</div>
</template>

View File

@ -1,17 +0,0 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@ -1,25 +0,0 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vue(),
vueJsx(),
],
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].js',
assetFileNames: 'assets/[name].css',
chunkFileNames: 'assets/[name].js'
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})