Skip to content
Merged
9 changes: 5 additions & 4 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
},
"dependencies": {
"@utpost/shared": "file:../shared",
"pinia": "^4.0.3",
"vue": "^3.5.40",
"vue-router": "^5.2.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/user-event": "^14.6.7",
"@testing-library/vue": "^8.1.0",
"@typescript-eslint/parser": "^8.71.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitest/eslint-plugin": "^1.6.27",
Expand All @@ -33,10 +37,7 @@
"prettier": "^3.9.5",
"vite": "^7.1.0",
"vitest": "^3.2.4",
"vue-eslint-parser": "^10.4.1",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/user-event": "^14.6.7",
"@testing-library/vue": "^8.1.0"
"vue-eslint-parser": "^10.4.1"
},
"engines": {
"node": "^22.18.0 || >=24.12.0"
Expand Down
88 changes: 5 additions & 83 deletions client/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,89 +1,11 @@
<script setup>
import { RouterLink, RouterView } from "vue-router";
<script setup lang="ts">
import { RouterView } from "vue-router";
import AppHeader from "./components/AppHeader.vue";
</script>

<template>
<header>
<div class="wrapper">
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/guides">Guides</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/tours">Tours</RouterLink>
</nav>
</div>
</header>

<AppHeader />
<RouterView />
</template>

<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}

.logo {
display: block;
margin: 0 auto 2rem;
}

nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
display: flex;
gap: 1rem;
align-items: center;
}

nav a {
padding: 0;
}

nav a.router-link-exact-active {
color: var(--color-text);
}

nav a.router-link-exact-active:hover {
background-color: transparent;
}

nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}

nav a:first-of-type {
border: 0;
}

@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}

.logo {
margin: 0 2rem 0 0;
}

header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}

nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;

padding: 1rem 0;
margin-top: 1rem;
}
}
</style>
<style scoped></style>
13 changes: 13 additions & 0 deletions client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,16 @@ export async function get<T>(path: string): Promise<T> {
if (!response.ok) throw new Error(`API-fel: ${response.status}`);
return (await response.json()) as T;
}

export async function post<TResponse, TBody = unknown>(
path: string,
body: TBody,
): Promise<TResponse> {
const response = await fetch(`${API_URL}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`API-fel: ${response.status}`);
return (await response.json()) as TResponse;
}
10 changes: 10 additions & 0 deletions client/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { post } from "../api";
import type { LoginRequest, LoginResponse } from "../../../shared/src/types";

export function login(credentials: LoginRequest): Promise<LoginResponse> {
return post<LoginResponse, LoginRequest>("/auth/login", credentials);
}

export function logout(): Promise<void> {
return post<void>("/auth/login", {});
}
102 changes: 102 additions & 0 deletions client/src/components/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<script setup lang="ts">
import { useRouter, RouterLink } from "vue-router";
import { useSessionStore } from "@/stores/session";

const session = useSessionStore();
const router = useRouter();

async function onLogout() {
await session.logout();
await router.push("/");
}
</script>

<template>
<header>
<div class="wrapper">
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/guides">Guides</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/tours">Tours</RouterLink>

<template v-if="session.isLoggedIn">
<RouterLink to="/profil">Min sida</RouterLink>
<button @click="onLogout">Logga ut</button>
</template>
<RouterLink v-else to="/logga-in">Logga in</RouterLink>
</nav>
</div>
</header>
</template>

<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}

.logo {
display: block;
margin: 0 auto 2rem;
}

nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
display: flex;
gap: 1rem;
align-items: center;
}

nav a {
padding: 0;
}

nav a.router-link-exact-active {
color: var(--color-text);
}

nav a.router-link-exact-active:hover {
background-color: transparent;
}

nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}

nav a:first-of-type {
border: 0;
}

@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}

.logo {
margin: 0 2rem 0 0;
}

header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}

nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;

padding: 1rem 0;
margin-top: 1rem;
}
}
</style>
3 changes: 3 additions & 0 deletions client/src/main.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import "./assets/main.css";

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";

const app = createApp(App);

app.use(createPinia());

app.use(router);

app.mount("#app");
6 changes: 6 additions & 0 deletions client/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import GuidesView from "../views/GuidesView.vue";
import GuideDetails from "../views/GuideDetails.vue";
import ToursView from "@/views/ToursView.vue";
import TourDetail from "@/views/TourDetail.vue";
import ProfileView from "@/views/ProfileView.vue";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
Expand Down Expand Up @@ -41,6 +42,11 @@ const router = createRouter({
name: "tour-detail",
component: TourDetail,
},
{
path: "/profil",
name: "profil",
component: ProfileView,
},
],
});

Expand Down
44 changes: 44 additions & 0 deletions client/src/stores/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";
import { login as apiLogin, logout as apiLogout } from "@/api/auth";
import type { LoginRequest, User } from "../../../shared/src/types";

export const useSessionStore = defineStore("session", () => {
const user = ref<User | null>(null);
const token = ref<string>();
const errorParagraph = ref<string | null>(null);
const loading = ref(false);

const isLoggedIn = computed(() => user.value !== null);

async function login(req: LoginRequest) {
loading.value = true;
errorParagraph.value = null;
try {
const data = await apiLogin(req);
user.value = data.user;
token.value = data.token;
localStorage.setItem("token", data.token);
localStorage.setItem("user", JSON.stringify(data.user));
} catch (error) {
console.log(
"Inloggningen misslyckades",
error instanceof Error ? error.message : error,
);
user.value = null;
errorParagraph.value = "Kunde inte logga in.";
} finally {
loading.value = false;
}
}

async function logout() {
try {
await apiLogout();
} finally {
user.value = null;
}
}

return { user, errorParagraph, loading, isLoggedIn, login, logout };
});
34 changes: 34 additions & 0 deletions client/src/views/LoginView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { useSessionStore } from "@/stores/session";

const session = useSessionStore();
const router = useRouter();
const email = ref<string>("");
const password_hash = ref<string>("");

const onSubmit = async () => {
await session.login({
email: email.value,
password_hash: password_hash.value,
});
if (session.isLoggedIn) await router.push("/profil");
};
</script>

<template>
<form @submit="onSubmit" class="login">
<h1>Logga in</h1>
<input v-model="email" placeholder="E-post" />
<input v-model="password_hash" type="password" placeholder="Lösenord" />
<p v-if="session.errorParagraph" class="error">
{{ session.errorParagraph }}
</p>
<button type="submit" class="button-blue" :disabled="session.loading">
Logga in
</button>
</form>
</template>

<style scoped></style>
15 changes: 15 additions & 0 deletions client/src/views/ProfileView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<div class="profile">
<h1>This is the profile page</h1>
</div>
</template>

<style>
@media (min-width: 1024px) {
.profile {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>
Loading
Loading