added settings and profile pages

This commit is contained in:
Edward Tirado Jr 2025-07-07 18:04:12 -05:00
parent 92b78e9c40
commit 56149f90b6
15 changed files with 451 additions and 34 deletions

View file

@ -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;
}

View 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>

View file

@ -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>

View 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>

View 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
View 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("/");
}
});

View file

@ -6,8 +6,11 @@
</template>
<script>
import Navbar from "~/components/common/navigation/navbar.vue";
export default {
name: "default",
components: { Navbar },
};
</script>

View file

@ -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
View file

@ -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",

View file

@ -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",

View file

@ -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>

View file

@ -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
View 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>

View 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>