added settings and profile pages
This commit is contained in:
parent
92b78e9c40
commit
56149f90b6
15 changed files with 451 additions and 34 deletions
|
@ -94,6 +94,12 @@ input {
|
|||
width: 80%; /* Could be more or less, depending on screen size */
|
||||
}
|
||||
|
||||
.page-header {
|
||||
font-size: 1.5rem;
|
||||
line-height: calc(2 / 1.5);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hover-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
119
src/components/ProfileMenu.vue
Normal file
119
src/components/ProfileMenu.vue
Normal file
|
@ -0,0 +1,119 @@
|
|||
<script lang="ts" setup>
|
||||
import { logout } from "~/composables/logout";
|
||||
|
||||
const isAuthenticated = ref(false);
|
||||
let isOpened = ref(false);
|
||||
const menuRef = ref<HTMLElement>();
|
||||
|
||||
const toggleMenu = function () {
|
||||
isOpened.value = !isOpened.value;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const handleClickOutside = (e: Event) => {
|
||||
if (!menuRef.value?.contains(e.target as Node)) {
|
||||
isOpened.value = false;
|
||||
} else {
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.classList.contains("menu-link") ||
|
||||
target.closest(".menu-link")
|
||||
) {
|
||||
isOpened.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="menuRef" class="profile-menu">
|
||||
<img
|
||||
alt="profile menu"
|
||||
class="profile-pic"
|
||||
src="https://placecage.lucidinternets.com/50/50"
|
||||
tabindex="0"
|
||||
@click="toggleMenu"
|
||||
@keydown.enter="toggleMenu"
|
||||
@keydown.space="toggleMenu"
|
||||
/>
|
||||
<div class="menu-content">
|
||||
<ul v-show="isOpened">
|
||||
<li role="none">
|
||||
<NuxtLink class="menu-link" to="/admin">Admin</NuxtLink>
|
||||
</li>
|
||||
<li role="none">
|
||||
<NuxtLink class="menu-link" to="/user/profile"> Profile</NuxtLink>
|
||||
</li>
|
||||
<li role="none">
|
||||
<NuxtLink class="menu-link" to="/user/settings"> Settings</NuxtLink>
|
||||
</li>
|
||||
<li
|
||||
id="logout"
|
||||
class="menu-link"
|
||||
role="none"
|
||||
tabindex="0"
|
||||
@click="logout"
|
||||
>
|
||||
Logout
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.profile-menu {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-pic:hover {
|
||||
border: 1px solid #6f0b51;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
min-width: 150px;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-content li {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu-content li:hover {
|
||||
background-color: #6f0b51;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menu-link {
|
||||
padding: 12px 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -1,16 +1,23 @@
|
|||
<template>
|
||||
<div class="grid grid-rows-2 text-center sm:text-left sm:grid-rows-none sm:grid-cols-2 my-5 navbar w-full">
|
||||
<div
|
||||
class="grid grid-rows-2 text-center sm:text-left sm:grid-rows-none sm:grid-cols-2 my-5 navbar w-full"
|
||||
>
|
||||
<NuxtLink class="block" to="/admin">
|
||||
<h1 class="block site-title bloodseeker">Cinema Corona</h1>
|
||||
</NuxtLink>
|
||||
|
||||
<ul class="mt-3 sm:mt-0 justify-self-center sm:justify-self-end inline-flex space-x-5 bloodseeker leading-10">
|
||||
<ul
|
||||
class="mt-3 sm:mt-0 justify-self-center sm:justify-self-end inline-flex space-x-5 bloodseeker leading-10"
|
||||
>
|
||||
<li>
|
||||
<NuxtLink class="text-xl header-link" to="/lists">Lists</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink class="text-xl header-link" to="/schedule">Schedule</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<ProfileMenu />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -18,9 +25,7 @@
|
|||
<script>
|
||||
export default {
|
||||
name: "navbar",
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
9
src/components/common/ui/FormButton.vue
Normal file
9
src/components/common/ui/FormButton.vue
Normal file
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<button class="btn p-3 mt-5" type="button">
|
||||
<slot> </slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
21
src/components/forms/PasswordReset.vue
Normal file
21
src/components/forms/PasswordReset.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts" setup>
|
||||
import FormButton from "~/components/common/ui/FormButton.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3 class="text-bold text-xl">Reset Password</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="old_password">Old Password</label>
|
||||
<input id="old_password" class="p-3" name="password" type="password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="password">New Password</label>
|
||||
<input id="password" class="p-3" name="password" type="password" />
|
||||
</div>
|
||||
<FormButton>Update Password</FormButton>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
26
src/composables/logout.ts
Normal file
26
src/composables/logout.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { useCookie } from "#app";
|
||||
|
||||
export function logout() {
|
||||
let config = useRuntimeConfig();
|
||||
fetch(`${config.public.apiURL}/auth/logout/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Token ${useCookie("token").value}`,
|
||||
},
|
||||
})
|
||||
.then((response) => response)
|
||||
.then((_json) => {
|
||||
let token = useCookie("token");
|
||||
token.value = null;
|
||||
navigateTo("/");
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const token = useCookie("token").value;
|
||||
if (!token) {
|
||||
navigateTo("/");
|
||||
}
|
||||
});
|
|
@ -6,8 +6,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Navbar from "~/components/common/navigation/navbar.vue";
|
||||
|
||||
export default {
|
||||
name: "default",
|
||||
components: { Navbar },
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -9,7 +9,12 @@ export default defineNuxtConfig({
|
|||
},
|
||||
},
|
||||
|
||||
modules: ["@nuxtjs/tailwindcss"],
|
||||
modules: ["@nuxtjs/tailwindcss", "@vesp/nuxt-fontawesome"],
|
||||
fontawesome: {
|
||||
icons: {
|
||||
solid: ["user"],
|
||||
},
|
||||
},
|
||||
css: ["@/assets/css/main.css"],
|
||||
compatibilityDate: "2025-04-05",
|
||||
|
||||
|
|
77
src/package-lock.json
generated
77
src/package-lock.json
generated
|
@ -9,8 +9,12 @@
|
|||
"lazysizes": "^5.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@nuxtjs/tailwindcss": "^6.2.0",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vesp/nuxt-fontawesome": "^1.2.1",
|
||||
"nuxt": "3.x",
|
||||
"prettier": "3.x",
|
||||
"typescript": "^5.8.3",
|
||||
|
@ -971,6 +975,68 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
|
||||
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
|
||||
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
|
||||
"dev": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
|
||||
"dev": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
|
||||
"dev": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
||||
|
@ -2827,6 +2893,17 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@vesp/nuxt-fontawesome": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@vesp/nuxt-fontawesome/-/nuxt-fontawesome-1.2.1.tgz",
|
||||
"integrity": "sha512-W7gaCQ8szFmOsMwBcxq22vyAV7wARQ8TK5wsd1we8Gt3KPFVQHj9ZYi738b4ePoeFxYGBEndh/uMLY6sIc+9HQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@nuxt/kit": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
|
||||
|
|
|
@ -8,8 +8,12 @@
|
|||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@nuxtjs/tailwindcss": "^6.2.0",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vesp/nuxt-fontawesome": "^1.2.1",
|
||||
"nuxt": "3.x",
|
||||
"prettier": "3.x",
|
||||
"typescript": "^5.8.3",
|
||||
|
|
|
@ -54,9 +54,9 @@ import AddMovie from "~/components/modal-content/AddMovie.vue";
|
|||
import Search from "~/components/admin/search.vue";
|
||||
import Showings from "~/components/admin/showings.vue";
|
||||
import Lists from "~/components/admin/lists.vue";
|
||||
import { useCookie } from "#app";
|
||||
import type { Movie } from "~/types/movie";
|
||||
import Modal from "~/components/Modal.vue";
|
||||
import Modal from "~/components/common/ui/Modal.vue";
|
||||
import { logout } from "~/composables/logout";
|
||||
|
||||
const modal_movie = defineModel<Movie>("#movie-modal");
|
||||
|
||||
|
@ -87,30 +87,6 @@ const toggleDisplay = function (element_id: string) {
|
|||
}
|
||||
});
|
||||
};
|
||||
const logout = () => {
|
||||
let config = useRuntimeConfig();
|
||||
fetch(`${config.public.apiURL}/auth/logout/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Token ${useCookie("token").value}`,
|
||||
},
|
||||
})
|
||||
.then((response) => response)
|
||||
.then((_json) => {
|
||||
let token = useCookie("token");
|
||||
token.value = null;
|
||||
navigateTo("/");
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const token = useCookie("token").value;
|
||||
if (!token) {
|
||||
navigateTo("/");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -78,7 +78,7 @@ import ShowMovie from "~/components/modal-content/ShowMovie.vue";
|
|||
import "lazysizes";
|
||||
import type { MovieList } from "~/types/movielist";
|
||||
import type { Movie } from "~/types/movie";
|
||||
import Modal from "~/components/Modal.vue";
|
||||
import Modal from "~/components/common/ui/Modal.vue";
|
||||
import { useCookie } from "#app";
|
||||
import { $fetch } from "ofetch";
|
||||
import MoviePoster from "~/components/MoviePoster.vue";
|
||||
|
|
116
src/pages/user/profile.vue
Normal file
116
src/pages/user/profile.vue
Normal file
|
@ -0,0 +1,116 @@
|
|||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<h2 class="page-header">Profile</h2>
|
||||
<div id="profile-card" class="movie-card neon-border">
|
||||
<div id="user-data">
|
||||
<div id="profile-picture">
|
||||
<img
|
||||
alt="profile image"
|
||||
class="user-icon neon-border"
|
||||
src="https://placecage.lucidinternets.com/g/200/200"
|
||||
/>
|
||||
</div>
|
||||
<ul class="profile-details">
|
||||
<li class="user-detail">
|
||||
<label for="name">Name</label>
|
||||
<span id="name">Eddie Tirado</span>
|
||||
</li>
|
||||
<li class="user-detail">
|
||||
<label for="username">Username</label>
|
||||
<span id="username">tiradoe@movienight.social</span>
|
||||
</li>
|
||||
<li class="user-detail">
|
||||
<label for="date-joined">Date Joined</label>
|
||||
<span id="date-joined">8 Feb 1984</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr class="neon-border my-5" />
|
||||
|
||||
<div id="extra-fields">
|
||||
<div id="reviews">
|
||||
<h3 class="section-header">Reviews</h3>
|
||||
<ul class="movie-review-list">
|
||||
<li class="movie-review">
|
||||
<span>The Room</span>
|
||||
<span>*****</span>
|
||||
<span>Best. Movie. Ever.</span>
|
||||
</li>
|
||||
<li class="movie-review">
|
||||
<span>Citizen Kane</span>
|
||||
<span>*</span>
|
||||
<span>Trash</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="movielists">
|
||||
<h3 class="section-header">Lists</h3>
|
||||
<ul id="movielist-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
label {
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 1.5rem;
|
||||
line-height: calc(2 / 1.5);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
#user-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
object-fit: cover;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
.profile-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
list-style: none;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.movie-card {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.movie-review {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#extra-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@media (width >= 48rem) {
|
||||
#user-data {
|
||||
flex-direction: row;
|
||||
gap: 5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
50
src/pages/user/settings.vue
Normal file
50
src/pages/user/settings.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts" setup>
|
||||
import PasswordResetForm from "~/components/forms/PasswordReset.vue";
|
||||
|
||||
const timezones = Intl.supportedValuesOf("timeZone");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="page-header">Settings</h2>
|
||||
|
||||
<div class="movie-card neon-border">
|
||||
<form action="#" class="flex flex-col gap-5">
|
||||
<label class="text-bold text-xl" for="site-name">Site Name</label>
|
||||
<input
|
||||
id="site-name"
|
||||
class="p-3"
|
||||
name="site-name"
|
||||
placeholder="Movie Night"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<h3 class="text-bold text-xl">Locale</h3>
|
||||
<!--SET TIMEZONE -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="timezone">Timezone</label>
|
||||
<select id="timezone" name="timezone">
|
||||
<option v-for="timezone in timezones" :value="timezone">
|
||||
{{ timezone }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr class="my-5 neon-border" />
|
||||
|
||||
<PasswordResetForm />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.movie-card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#timezone {
|
||||
color: black;
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue