Progress?
This commit is contained in:
parent
4791a5ef1d
commit
211e93b685
32 changed files with 752 additions and 38 deletions
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Vuetify 3</title>
|
||||
<title>ScheduleTogether</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
BIN
stfrontend/public/images/background.jpg
Normal file
BIN
stfrontend/public/images/background.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 163 KiB |
BIN
stfrontend/public/images/background2.jpg
Normal file
BIN
stfrontend/public/images/background2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 265 KiB |
BIN
stfrontend/public/images/background3.jpg
Normal file
BIN
stfrontend/public/images/background3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
BIN
stfrontend/public/images/background4.jpg
Normal file
BIN
stfrontend/public/images/background4.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 248 KiB |
4
stfrontend/public/images/credits.md
Normal file
4
stfrontend/public/images/credits.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
background.jpg - Vojta Kovařík @ pexels - Green Leafed Plant With Water Drops
|
||||
background2.jpg - Pixabay @ pexels - Brown Leafed Tree on Open Field Under White Clouds and Blue Sky
|
||||
background3.jpg - Stephan Seeber @ pexels - Scenic View Of Mountains During Dawn
|
||||
background4.jpg - Roberto Nickson @ pexels - Brown Mountains
|
|
@ -8,4 +8,4 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
</script>
|
3
stfrontend/src/components.d.ts
vendored
3
stfrontend/src/components.d.ts
vendored
|
@ -10,8 +10,11 @@ declare module 'vue' {
|
|||
AppBar: typeof import('./components/AppBar.vue')['default']
|
||||
AppFooter: typeof import('./components/AppFooter.vue')['default']
|
||||
AppHeader: typeof import('./components/AppHeader.vue')['default']
|
||||
copy: typeof import('./components/LoginForm copy.vue')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
LoginForm: typeof import('./components/LoginForm.vue')['default']
|
||||
NavBar: typeof import('./components/NavBar.vue')['default']
|
||||
RegisterForm: typeof import('./components/RegisterForm.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
|
|
|
@ -1,35 +1,39 @@
|
|||
<template>
|
||||
<v-toolbar
|
||||
class="ma-2"
|
||||
:collapse="false"
|
||||
density="comfortable"
|
||||
floating
|
||||
dark
|
||||
rounded="xl"
|
||||
elevation="8"
|
||||
>
|
||||
<v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
|
||||
<v-toolbar
|
||||
class="ma-2"
|
||||
:collapse="false"
|
||||
density="comfortable"
|
||||
color="grey-darken-4"
|
||||
floating
|
||||
dark
|
||||
rounded="xl"
|
||||
elevation="8"
|
||||
style="width: calc(100% - 16px)"
|
||||
>
|
||||
<v-app-bar-nav-icon @click.stop="drawer = !drawer" />
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
|
||||
<v-toolbar-title class="text-h5 text-center">ScheduleTogether</v-toolbar-title>
|
||||
<v-toolbar-title class="text-h5 text-center">
|
||||
ScheduleTogether
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-btn icon>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:location="$vuetify.display.mobile ? 'bottom' : 'left'"
|
||||
temporary
|
||||
>
|
||||
<v-list
|
||||
:items="items"
|
||||
></v-list>
|
||||
</v-navigation-drawer>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:location="$vuetify.display.mobile ? 'bottom' : 'left'"
|
||||
temporary
|
||||
>
|
||||
<v-list
|
||||
:items="items"
|
||||
/>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -19,15 +19,15 @@
|
|||
class="text-caption text-disabled"
|
||||
style="position: absolute; right: 16px;"
|
||||
>
|
||||
© 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
|
||||
© 2023-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Zervo Network</span>
|
||||
—
|
||||
<a
|
||||
class="text-decoration-none on-surface"
|
||||
href="https://vuetifyjs.com/about/licensing/"
|
||||
href="https://www.gnu.org/licenses/agpl-3.0.en.html"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
MIT License
|
||||
AGPL v3 license
|
||||
</a>
|
||||
</div>
|
||||
</v-footer>
|
||||
|
|
69
stfrontend/src/components/LoginForm.vue
Normal file
69
stfrontend/src/components/LoginForm.vue
Normal file
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div class="text-h4 text-center">
|
||||
Sign in
|
||||
</div>
|
||||
<br>
|
||||
<v-form
|
||||
v-model="form"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
:readonly="loading"
|
||||
:rules="[required]"
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
label="Email"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:readonly="loading"
|
||||
:rules="[required]"
|
||||
variant="outlined"
|
||||
label="Password"
|
||||
:append-inner-icon="passwordvis ? 'mdi-eye-outline' : 'mdi-eye-off-outline'"
|
||||
:type="passwordvis ? 'text' : 'password'"
|
||||
placeholder="Enter your password"
|
||||
clearable
|
||||
@click:append-inner="() => (passwordvis = !passwordvis)"
|
||||
/>
|
||||
|
||||
<br>
|
||||
|
||||
<v-btn
|
||||
:disabled="!form"
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="elevated"
|
||||
block
|
||||
>
|
||||
Sign In
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const form = ref<boolean>(false);
|
||||
const email = ref<string | null>(null);
|
||||
const password = ref<string | null>(null);
|
||||
const passwordvis = ref<boolean>(false);
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
function onSubmit () {
|
||||
if (!form.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
setTimeout(() => (loading.value = false), 2000)
|
||||
}
|
||||
|
||||
function required (v: never) {
|
||||
return !!v || 'Field is required'
|
||||
}
|
||||
</script>
|
128
stfrontend/src/components/RegisterForm.vue
Normal file
128
stfrontend/src/components/RegisterForm.vue
Normal file
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<div class="text-h4 text-center">
|
||||
Sign up
|
||||
</div>
|
||||
<br>
|
||||
<v-form
|
||||
v-model="form"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
:readonly="loading"
|
||||
:rules="[required,checkemail]"
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
label="Email"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
:readonly="loading"
|
||||
:rules="[required,checkuser]"
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
label="Username"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:readonly="loading"
|
||||
:rules="[required,checkpass]"
|
||||
variant="outlined"
|
||||
label="Password"
|
||||
:append-inner-icon="passwordvis ? 'mdi-eye-outline' : 'mdi-eye-off-outline'"
|
||||
:type="passwordvis ? 'text' : 'password'"
|
||||
placeholder="Enter your password"
|
||||
clearable
|
||||
@click:append-inner="() => (passwordvis = !passwordvis)"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="passwordagain"
|
||||
:readonly="loading"
|
||||
:rules="[required,passmatch]"
|
||||
variant="outlined"
|
||||
label="Repeat Password"
|
||||
:append-inner-icon="passwordvis ? 'mdi-eye-outline' : 'mdi-eye-off-outline'"
|
||||
:type="passwordvis ? 'text' : 'password'"
|
||||
placeholder="Enter password again"
|
||||
clearable
|
||||
@click:append-inner="() => (passwordvis = !passwordvis)"
|
||||
/>
|
||||
|
||||
<br>
|
||||
|
||||
<v-btn
|
||||
:disabled="!form"
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="elevated"
|
||||
block
|
||||
>
|
||||
Register
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const form = ref<boolean>(false);
|
||||
const email = ref<string | null>(null);
|
||||
const username = ref<string | null>(null);
|
||||
const password = ref<string | null>(null);
|
||||
const passwordagain = ref<string | null>(null);
|
||||
const passwordvis = ref<boolean>(false);
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
function onSubmit () {
|
||||
if (!form.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
setTimeout(() => (loading.value = false), 2000)
|
||||
}
|
||||
|
||||
function required (v: never) {
|
||||
return !!v || 'Field is required'
|
||||
}
|
||||
|
||||
function checkpass (v: string) {
|
||||
if (v.length < 5) {
|
||||
return 'Must be at least 5 characters long'
|
||||
}
|
||||
else if (v.length > 128) {
|
||||
return 'Can be no longer than 128 characters'
|
||||
}
|
||||
else if (!v.toLowerCase().match(/^(?=.*[A-Za-z])(?=.*\d).+$/)) {
|
||||
return 'Must contain letters and numbers'
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function passmatch (v: never) {
|
||||
return v==password.value || 'Passwords must match'
|
||||
}
|
||||
|
||||
function checkemail (v: string) {
|
||||
return (v.toLowerCase()
|
||||
.match(
|
||||
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
|
||||
) && v.length < 100) || 'Must be a valid email address'
|
||||
}
|
||||
|
||||
function checkuser (v: string) {
|
||||
if (v.length < 3 || v.length > 18) {
|
||||
return 'Must be between 3-18 characters long'
|
||||
}
|
||||
else if (!v.toLowerCase().match(/^[A-Za-z0-9_.-]+$/)) {
|
||||
return 'Can only contain letters a-Z, numbers and . - _'
|
||||
}
|
||||
return true
|
||||
}
|
||||
</script>
|
29
stfrontend/src/helpers/config-loader.ts
Normal file
29
stfrontend/src/helpers/config-loader.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
const configUrl = `${window.location.origin}/config.json`; // Get the base URL dynamically
|
||||
|
||||
let apiUrl: string | null = null;
|
||||
|
||||
export const getApiUrl = (): string => {
|
||||
if (apiUrl) {
|
||||
// If the API URL has already been fetched, return it immediately
|
||||
return apiUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', configUrl, false); // Synchronous XHR request
|
||||
xhr.send();
|
||||
|
||||
if (xhr.status !== 200) {
|
||||
throw new Error(`Network response was not ok: ${xhr.status}`);
|
||||
}
|
||||
|
||||
const configData = JSON.parse(xhr.responseText);
|
||||
apiUrl = configData.apiUrl; // Extract the API URL
|
||||
return apiUrl || "";
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function to initialize the apiUrl
|
||||
getApiUrl();
|
56
stfrontend/src/helpers/fetch-wrapper.ts
Normal file
56
stfrontend/src/helpers/fetch-wrapper.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { useAuthStore } from '@/stores'
|
||||
import { getApiUrl } from './config-loader';
|
||||
|
||||
export const fetchWrapper = {
|
||||
get: request('GET'),
|
||||
post: request('POST'),
|
||||
put: request('PUT'),
|
||||
delete: request('DELETE')
|
||||
}
|
||||
|
||||
function request(method: string) {
|
||||
return (url: string, body: object | undefined = undefined) => {
|
||||
const requestOptions: RequestInit = {
|
||||
body: undefined,
|
||||
method,
|
||||
headers: getAuthHeader(url) || {} as Record<string, string>
|
||||
};
|
||||
if (body) {
|
||||
(requestOptions.headers as Record<string, string>)['Content-Type'] = 'application/json';
|
||||
requestOptions.body = JSON.stringify(body);
|
||||
}
|
||||
return fetch(url, requestOptions).then(handleResponse);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResponse(response: Response) {
|
||||
const isJson = response.headers?.get('content-type')?.includes('application/json');
|
||||
const data = isJson ? await response.json() : null;
|
||||
|
||||
// check for error response
|
||||
if (!response.ok) {
|
||||
const { user } = useAuthStore();
|
||||
if ([401, 403].includes(response.status) && user) {
|
||||
// auto logout if 401 Unauthorized or 403 Forbidden response
|
||||
//logout();
|
||||
}
|
||||
|
||||
// get error message from body or default to response status
|
||||
const error = (data && data.error) || response.status;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getAuthHeader(url: string): Record<string, string> {
|
||||
const { auth } = useAuthStore();
|
||||
const isLoggedIn: boolean = !!auth?.token;
|
||||
const apiUrl: string = getApiUrl();
|
||||
const isApiUrl = url.startsWith(apiUrl);
|
||||
if (isLoggedIn && isApiUrl) {
|
||||
return { Authorization: `Bearer ${auth.token}`}
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
158
stfrontend/src/layouts/action.vue
Normal file
158
stfrontend/src/layouts/action.vue
Normal file
|
@ -0,0 +1,158 @@
|
|||
<template>
|
||||
<ul class="bgcircles">
|
||||
<li />
|
||||
<li />
|
||||
<li />
|
||||
<li />
|
||||
<li />
|
||||
<li />
|
||||
<li />
|
||||
<li />
|
||||
<li />
|
||||
<li />
|
||||
<li />
|
||||
</ul>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
class="fill-height"
|
||||
>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<v-col
|
||||
class="d-flex justify-center"
|
||||
>
|
||||
<router-view />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-main>
|
||||
<AppFooter />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Original background effect by Mohammed Abdul Mohaiman*/
|
||||
|
||||
.bgcircles{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bgcircles li{
|
||||
position: absolute;
|
||||
display: block;
|
||||
list-style: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
animation: bganimate 25s linear infinite;
|
||||
bottom: -150px;
|
||||
|
||||
}
|
||||
|
||||
.bgcircles li:nth-child(1){
|
||||
left: 25%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
|
||||
.bgcircles li:nth-child(2){
|
||||
left: 10%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation-delay: 2s;
|
||||
animation-duration: 12s;
|
||||
}
|
||||
|
||||
.bgcircles li:nth-child(3){
|
||||
left: 70%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.bgcircles li:nth-child(4){
|
||||
left: 40%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 18s;
|
||||
}
|
||||
|
||||
.bgcircles li:nth-child(5){
|
||||
left: 65%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.bgcircles li:nth-child(6){
|
||||
left: 75%;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
.bgcircles li:nth-child(7){
|
||||
left: 35%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
animation-delay: 7s;
|
||||
}
|
||||
|
||||
.bgcircles li:nth-child(8){
|
||||
left: 50%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
animation-delay: 15s;
|
||||
animation-duration: 45s;
|
||||
}
|
||||
|
||||
.bgcircles li:nth-child(9){
|
||||
left: 20%;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
animation-delay: 2s;
|
||||
animation-duration: 35s;
|
||||
}
|
||||
|
||||
.bgcircles li:nth-child(10){
|
||||
left: 85%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 11s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes bganimate {
|
||||
|
||||
0%{
|
||||
transform: translateY(0) rotate(0deg);
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
100%{
|
||||
transform: translateY(-1000px) rotate(720deg);
|
||||
opacity: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
24
stfrontend/src/layouts/appdefault.vue
Normal file
24
stfrontend/src/layouts/appdefault.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<v-img
|
||||
cover
|
||||
src="/images/background2.jpg"
|
||||
>
|
||||
<AppBar />
|
||||
|
||||
<v-main>
|
||||
<div
|
||||
class="ma-2 mt-1"
|
||||
>
|
||||
<router-view />
|
||||
</div>
|
||||
</v-main>
|
||||
</v-img>
|
||||
|
||||
<AppFooter />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
|
@ -1,11 +1,22 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<AppBar />
|
||||
|
||||
<v-main>
|
||||
<router-view />
|
||||
<v-container
|
||||
fluid
|
||||
class="fill-height"
|
||||
>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<v-col
|
||||
class="d-flex justify-center"
|
||||
>
|
||||
<router-view />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<AppFooter />
|
||||
</v-app>
|
||||
</template>
|
||||
|
|
19
stfrontend/src/pages/dashboard.vue
Normal file
19
stfrontend/src/pages/dashboard.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<v-sheet
|
||||
class="pa-4"
|
||||
color="grey-darken-4"
|
||||
elevation="18"
|
||||
rounded="xl"
|
||||
>
|
||||
<HelloWorld />
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: appdefault
|
||||
</route>
|
|
@ -1,7 +1,16 @@
|
|||
<template>
|
||||
<HelloWorld />
|
||||
<v-sheet
|
||||
class="pa-6"
|
||||
rounded="lg"
|
||||
elevation="18"
|
||||
>
|
||||
<div class="text-h4">
|
||||
Please wait ...
|
||||
</div>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
import router from '@/router';
|
||||
router.push('login')
|
||||
</script>
|
32
stfrontend/src/pages/login.vue
Normal file
32
stfrontend/src/pages/login.vue
Normal file
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<v-sheet
|
||||
class="pa-6"
|
||||
color="grey-darken-4"
|
||||
elevation="18"
|
||||
rounded="xl"
|
||||
fluid
|
||||
:width="500"
|
||||
:max-width="$vuetify.display.mobile ? 350 : 500"
|
||||
>
|
||||
<LoginForm />
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-body-1 text-center">
|
||||
Don't have an account? <router-link :to="'register'">Sign up</router-link> instead.
|
||||
</div>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: action
|
||||
</route>
|
32
stfrontend/src/pages/register.vue
Normal file
32
stfrontend/src/pages/register.vue
Normal file
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<v-sheet
|
||||
class="pa-6"
|
||||
color="grey-darken-4"
|
||||
elevation="18"
|
||||
rounded="xl"
|
||||
fluid
|
||||
:width="500"
|
||||
:max-width="$vuetify.display.mobile ? 350 : 500"
|
||||
>
|
||||
<RegisterForm />
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-body-1 text-center">
|
||||
Already have an account? <router-link :to="'login'">Sign in</router-link> instead.
|
||||
</div>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: action
|
||||
</route>
|
65
stfrontend/src/stores/alert.ts
Normal file
65
stfrontend/src/stores/alert.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
// Alert
|
||||
import type { AlertData } from '@/types';
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAlertStore = defineStore('alert', {
|
||||
state: () => ({
|
||||
alerts: [] as Array<AlertData>,
|
||||
}),
|
||||
actions: {
|
||||
addAlert(message: string, type: string, timeout: number = 0) {
|
||||
// If more than 10 alerts, remove oldest
|
||||
if (this.alerts.length > 10) {
|
||||
this.alerts.shift();
|
||||
}
|
||||
|
||||
// Define alert and assign unique ID
|
||||
const alert: AlertData = { message, type, timeout, id: Date.now(), timeoutId: null}
|
||||
this.alerts.push(alert);
|
||||
|
||||
// Register timeout for alert
|
||||
if (timeout > 0) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.removeSpecific(alert.id);
|
||||
}, timeout);
|
||||
alert.timeoutId = timeoutId;
|
||||
}
|
||||
},
|
||||
success(message: string, timeout = 0) {
|
||||
// add green alert (success)
|
||||
this.addAlert(message, 'alert-success', timeout);
|
||||
},
|
||||
error(message: string, timeout = 0) {
|
||||
// add red alert (error)
|
||||
this.addAlert(message, 'alert-danger', timeout);
|
||||
},
|
||||
removeOldest() {
|
||||
const oldestAlert = this.alerts.shift();
|
||||
// Remove timeout for this alert (avoid errors)
|
||||
if (oldestAlert && oldestAlert.timeoutId) {
|
||||
clearTimeout(oldestAlert.timeoutId);
|
||||
}
|
||||
},
|
||||
removeSpecific(alertId: number) {
|
||||
// Find alert by index
|
||||
const index = this.alerts.findIndex((alert) => alert.id === alertId);
|
||||
if (index !== -1) {
|
||||
const alert = this.alerts[index];
|
||||
this.alerts.splice(index, 1);
|
||||
|
||||
// Remove timeout for this alert (avoid errors)
|
||||
if (alert.timeoutId) {
|
||||
clearTimeout(alert.timeoutId);
|
||||
}
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
for (const alert of this.alerts) {
|
||||
if (alert.timeoutId) {
|
||||
clearTimeout(alert.timeoutId)
|
||||
}
|
||||
}
|
||||
this.alerts = [];
|
||||
}
|
||||
}
|
||||
})
|
37
stfrontend/src/stores/auth.ts
Normal file
37
stfrontend/src/stores/auth.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
// Utilities
|
||||
import { defineStore } from 'pinia'
|
||||
import type { Auth, DetailedUser } from '@/types'
|
||||
import { fetchWrapper } from '@/helpers/fetch-wrapper'
|
||||
import { getApiUrl } from '@/helpers/config-loader';
|
||||
import { useAlertStore } from './alert';
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
auth: JSON.parse(localStorage.getItem('auth') || "{}") as Auth,
|
||||
user: JSON.parse(localStorage.getItem('this_user') || "{}") as DetailedUser,
|
||||
}),
|
||||
actions: {
|
||||
async login(username: string, password: string) {
|
||||
try {
|
||||
const data = await fetchWrapper.post(`${getApiUrl()}/account/signin`, { username, password})
|
||||
const token: string = data.token || ""
|
||||
|
||||
if (token == null) {
|
||||
throw new Error("Could not sign in, didn't receive token");
|
||||
}
|
||||
|
||||
this.auth = {
|
||||
token
|
||||
}
|
||||
|
||||
const user: DetailedUser = await fetchWrapper.get(`${getApiUrl()}/users/self`);
|
||||
this.user = user;
|
||||
|
||||
localStorage.setItem('this_user', JSON.stringify(user));
|
||||
} catch (error) {
|
||||
const alertStore = useAlertStore();
|
||||
alertStore.error((error as Error).message, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -2,3 +2,6 @@
|
|||
import { createPinia } from 'pinia'
|
||||
|
||||
export default createPinia()
|
||||
|
||||
export * from './auth';
|
||||
export * from './alert';
|
3
stfrontend/src/typed-router.d.ts
vendored
3
stfrontend/src/typed-router.d.ts
vendored
|
@ -19,5 +19,8 @@ declare module 'vue-router/auto-routes' {
|
|||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/dashboard': RouteRecordInfo<'/dashboard', '/dashboard', Record<never, never>, Record<never, never>>,
|
||||
'/login': RouteRecordInfo<'/login', '/login', Record<never, never>, Record<never, never>>,
|
||||
'/register': RouteRecordInfo<'/register', '/register', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
|
7
stfrontend/src/types/alert.ts
Normal file
7
stfrontend/src/types/alert.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type AlertData = {
|
||||
message: string,
|
||||
type: string,
|
||||
timeout: number,
|
||||
id: number,
|
||||
timeoutId: number | null,
|
||||
}
|
3
stfrontend/src/types/auth.ts
Normal file
3
stfrontend/src/types/auth.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export type Auth = {
|
||||
token: string;
|
||||
}
|
3
stfrontend/src/types/index.ts
Normal file
3
stfrontend/src/types/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './auth';
|
||||
export * from './user';
|
||||
export * from './alert'
|
10
stfrontend/src/types/user.ts
Normal file
10
stfrontend/src/types/user.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export type DetailedUser = {
|
||||
id: number,
|
||||
uuid: string,
|
||||
username: string,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
email: string,
|
||||
verified: boolean,
|
||||
friends: Array<string>
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
|
Loading…
Reference in a new issue