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">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Welcome to Vuetify 3</title>
|
<title>ScheduleTogether</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<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
|
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']
|
AppBar: typeof import('./components/AppBar.vue')['default']
|
||||||
AppFooter: typeof import('./components/AppFooter.vue')['default']
|
AppFooter: typeof import('./components/AppFooter.vue')['default']
|
||||||
AppHeader: typeof import('./components/AppHeader.vue')['default']
|
AppHeader: typeof import('./components/AppHeader.vue')['default']
|
||||||
|
copy: typeof import('./components/LoginForm copy.vue')['default']
|
||||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||||
|
LoginForm: typeof import('./components/LoginForm.vue')['default']
|
||||||
NavBar: typeof import('./components/NavBar.vue')['default']
|
NavBar: typeof import('./components/NavBar.vue')['default']
|
||||||
|
RegisterForm: typeof import('./components/RegisterForm.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,39 @@
|
||||||
<template>
|
<template>
|
||||||
<v-toolbar
|
<v-toolbar
|
||||||
class="ma-2"
|
class="ma-2"
|
||||||
:collapse="false"
|
:collapse="false"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
floating
|
color="grey-darken-4"
|
||||||
dark
|
floating
|
||||||
rounded="xl"
|
dark
|
||||||
elevation="8"
|
rounded="xl"
|
||||||
>
|
elevation="8"
|
||||||
<v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
|
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-btn icon>
|
||||||
<v-icon>mdi-account</v-icon>
|
<v-icon>mdi-account</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<v-navigation-drawer
|
<v-navigation-drawer
|
||||||
v-model="drawer"
|
v-model="drawer"
|
||||||
:location="$vuetify.display.mobile ? 'bottom' : 'left'"
|
:location="$vuetify.display.mobile ? 'bottom' : 'left'"
|
||||||
temporary
|
temporary
|
||||||
>
|
>
|
||||||
<v-list
|
<v-list
|
||||||
:items="items"
|
:items="items"
|
||||||
></v-list>
|
/>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -19,15 +19,15 @@
|
||||||
class="text-caption text-disabled"
|
class="text-caption text-disabled"
|
||||||
style="position: absolute; right: 16px;"
|
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
|
<a
|
||||||
class="text-decoration-none on-surface"
|
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"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
MIT License
|
AGPL v3 license
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</v-footer>
|
</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>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<AppBar />
|
|
||||||
|
|
||||||
<v-main>
|
<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>
|
</v-main>
|
||||||
|
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<HelloWorld />
|
<v-sheet
|
||||||
|
class="pa-6"
|
||||||
|
rounded="lg"
|
||||||
|
elevation="18"
|
||||||
|
>
|
||||||
|
<div class="text-h4">
|
||||||
|
Please wait ...
|
||||||
|
</div>
|
||||||
|
</v-sheet>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
//
|
import router from '@/router';
|
||||||
|
router.push('login')
|
||||||
</script>
|
</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'
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
export default createPinia()
|
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 {
|
export interface RouteNamedMap {
|
||||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
'/': 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"],
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
"exclude": ["src/**/__tests__/*"],
|
"exclude": ["src/**/__tests__/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
},
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"playwright.config.*"
|
"playwright.config.*"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
|
Loading…
Reference in a new issue