Initial commit
This commit is contained in:
70
frontend/src/App.vue
Normal file
70
frontend/src/App.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div v-if="isAuthenticated" class="navigation">
|
||||
Verein:
|
||||
<select v-model="selectedClub">
|
||||
<option value="">---</option>
|
||||
<option value="new">Neuer Verein</option>
|
||||
<option v-for="club in clubs" :key="club.id" :value="club.id">{{ club.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import apiClient from './apiClient.js';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
data() {
|
||||
return {
|
||||
selectedClub: null, // Initialisiere selectedClub als null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs']),
|
||||
},
|
||||
watch: {
|
||||
selectedClub(newVal) {
|
||||
console.log('selectedClub watcher:', newVal);
|
||||
this.setCurrentClub(newVal);
|
||||
},
|
||||
currentClub(newVal) {
|
||||
console.log('currentClub watcher:', newVal);
|
||||
if (newVal === 'new') {
|
||||
this.$router.push('/createclub');
|
||||
} else if (newVal) {
|
||||
this.$router.push(`/showclub/${newVal}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setCurrentClub', 'setClubs']),
|
||||
},
|
||||
async mounted() {
|
||||
const response = await apiClient.get('/clubs');
|
||||
this.setClubs(response.data);
|
||||
if (this.currentClub) {
|
||||
this.selectedClub = this.currentClub;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
.navigation {
|
||||
width: 10em;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
router-view {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
22
frontend/src/apiClient.js
Normal file
22
frontend/src/apiClient.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import axios from 'axios';
|
||||
import store from './store';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: 'http://localhost:3000/api',
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(config => {
|
||||
const token = store.getters.token;
|
||||
const user = store.getters.username;
|
||||
|
||||
if (token) {
|
||||
config.headers['authcode'] = token;
|
||||
}
|
||||
if (user) {
|
||||
config.headers['userid'] = user;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
export default apiClient;
|
||||
16
frontend/src/assets/css/main.scss
Normal file
16
frontend/src/assets/css/main.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
h1 {
|
||||
border-bottom: 1px solid #000000;
|
||||
}
|
||||
#app {
|
||||
flex: 1;
|
||||
}
|
||||
1
frontend/src/assets/vue.svg
Normal file
1
frontend/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
43
frontend/src/components/HelloWorld.vue
Normal file
43
frontend/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
10
frontend/src/main.js
Normal file
10
frontend/src/main.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import '@/assets/css/main.scss';
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.use(store)
|
||||
.mount('#app');
|
||||
23
frontend/src/router.js
Normal file
23
frontend/src/router.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import Register from './views/Register.vue';
|
||||
import Login from './views/Login.vue';
|
||||
import Activate from './views/Activate.vue';
|
||||
import Home from './views/Home.vue';
|
||||
import CreateClub from './views/CreateClub.vue';
|
||||
import ClubView from './views/ClubView.vue';
|
||||
|
||||
const routes = [
|
||||
{ path: '/register', component: Register },
|
||||
{ path: '/login', component: Login },
|
||||
{ path: '/activate/:activationCode', component: Activate },
|
||||
{ path: '/', component: Home },
|
||||
{ path: '/createclub', component: CreateClub },
|
||||
{ path: '/showclub/:1', component: ClubView },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
65
frontend/src/store.js
Normal file
65
frontend/src/store.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createStore } from 'vuex';
|
||||
|
||||
const store = createStore({
|
||||
state: {
|
||||
token: localStorage.getItem('token') || null,
|
||||
username: localStorage.getItem('username') || '',
|
||||
currentClub: localStorage.getItem('currentClub') || null,
|
||||
clubs: localStorage.getItem('clubs') || [],
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token) {
|
||||
state.token = token;
|
||||
localStorage.setItem('token', token);
|
||||
state.currentClub = null;
|
||||
localStorage.setItem('currentClub', null);
|
||||
},
|
||||
setUsername(state, username) {
|
||||
state.username = username;
|
||||
localStorage.setItem('username', username);
|
||||
},
|
||||
setClub(state, club) {
|
||||
state.currentClub = club;
|
||||
localStorage.setItem('currentClub', club);
|
||||
},
|
||||
setClubsMutation(state, clubs) {
|
||||
state.clubs = clubs;
|
||||
localStorage.setItem('clubs', clubs);
|
||||
},
|
||||
clearToken(state) {
|
||||
state.token = null;
|
||||
localStorage.removeItem('token');
|
||||
router.push("/");
|
||||
},
|
||||
clearUsername(state) {
|
||||
state.username = '';
|
||||
localStorage.removeItem('username');
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
login({ commit }, { token, username }) {
|
||||
commit('setToken', token);
|
||||
commit('setUsername', username);
|
||||
},
|
||||
logout({ commit }) {
|
||||
commit('clearToken');
|
||||
commit('clearUsername');
|
||||
},
|
||||
setCurrentClub({ commit }, club) {
|
||||
console.log('action', club);
|
||||
commit('setClub', club);
|
||||
},
|
||||
setClubs({ commit }, clubs) {
|
||||
commit('setClubsMutation', clubs);
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
isAuthenticated: state => !!state.token,
|
||||
token: state => state.token,
|
||||
username: state => state.username,
|
||||
currentClub: state => state.currentClub,
|
||||
clubs: state => state.clubs,
|
||||
},
|
||||
});
|
||||
|
||||
export default store;
|
||||
79
frontend/src/style.css
Normal file
79
frontend/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
27
frontend/src/views/Activate.vue
Normal file
27
frontend/src/views/Activate.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Activate Account</h2>
|
||||
<button @click="activate">Activate</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
async activate() {
|
||||
try {
|
||||
const activationCode = this.$route.params.activationCode;
|
||||
await axios.get(`/api/auth/activate/${activationCode}`);
|
||||
alert('Account activated! You can now log in.');
|
||||
this.$router.push('/login');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Activation failed.');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
16
frontend/src/views/ClubView.vue
Normal file
16
frontend/src/views/ClubView.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<h2>Verein {{ club.name }}</h2>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ClubView",
|
||||
data() {
|
||||
return {
|
||||
club: {
|
||||
name: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
42
frontend/src/views/CreateClub.vue
Normal file
42
frontend/src/views/CreateClub.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Verein anlegen</h2>
|
||||
<label>Name des Vereins: <input type="text" v-model="clubName" /></label>
|
||||
<button @click="createClub">Verein anlegen</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex/dist/vuex.cjs.js';
|
||||
import apiClient from '../apiClient';
|
||||
|
||||
export default {
|
||||
name: "CreateClubView",
|
||||
data() {
|
||||
return {
|
||||
clubName: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setClubs', 'setCurrentClub']),
|
||||
async createClub() {
|
||||
if (this.clubName.trim().length < 3) {
|
||||
alert('Bitte gib dem Verein einen Aussagekräftigen Namen');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newClub = await apiClient.post('/clubs', { name: this.clubName });
|
||||
const clubsResponse = await apiClient.get('/clubs');
|
||||
this.setClubs(clubsResponse.data);
|
||||
this.setCurrentClub(newClub.data);
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
alert('Der Verein existiert bereits.');
|
||||
} else {
|
||||
alert('Ein unbekannter Fehler ist aufgetreten.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
20
frontend/src/views/Home.vue
Normal file
20
frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Home</h2>
|
||||
<p v-if="!isAuthenticated">You are not logged in. <router-link to="/login">Login</router-link> or <router-link to="/register">Register</router-link></p>
|
||||
<p v-else>Welcome! <button @click="logout">Logout</button></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['logout']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
39
frontend/src/views/Login.vue
Normal file
39
frontend/src/views/Login.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Login</h2>
|
||||
<form @submit.prevent="executeLogin">
|
||||
<input v-model="email" type="email" placeholder="Email" required />
|
||||
<input v-model="password" type="password" placeholder="Password" required />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login']),
|
||||
async executeLogin() {
|
||||
try {
|
||||
const response = await axios.post('http://localhost:3000/api/auth/login', { email: this.email, password: this.password }, {
|
||||
timeout: 5000,
|
||||
});
|
||||
await this.login({ token: response.data.token, username: this.email });
|
||||
this.$router.push('/');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Login failed.');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
36
frontend/src/views/Register.vue
Normal file
36
frontend/src/views/Register.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2>Register</h2>
|
||||
<form @submit.prevent="register">
|
||||
<input v-model="email" type="email" placeholder="Email" required />
|
||||
<input v-model="password" type="password" placeholder="Password" required />
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async register() {
|
||||
try {
|
||||
await axios.post('/api/auth/register', { email: this.email, password: this.password });
|
||||
alert('Registration successful! Please check your email to activate your account.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Registration failed.');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user