initial commit

This commit is contained in:
Edward Tirado Jr 2026-02-16 19:12:00 -06:00
commit 869be69d67
42 changed files with 11444 additions and 0 deletions

View file

@ -0,0 +1,11 @@
<script lang="ts" setup>
</script>
<template>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,18 @@
<script lang="ts" setup>
const props = defineProps<{
title: string
}>()
</script>
<template>
<h2>{{ props.title }}</h2>
</template>
<style scoped>
h1 {
font-size: 2rem;
font-weight: bold;
margin-bottom: 1rem;
}
</style>

View file

@ -0,0 +1,32 @@
<script lang="ts" setup>
</script>
<template>
<form>
<label for="list_name">Add List</label>
<div>
<input class="" name="list_name"
placeholder="List Name"
type="text">
<button>Add</button>
</div>
</form>
</template>
<style scoped>
button {
background-color: #4caf50;
color: white;
padding: .5rem 1rem;
border: none;
border-radius: 0 4px 4px 0;
}
input {
padding: .5rem;
border: 1px solid #ccc;
border-right: none;
border-radius: 4px 0 0 4px;
}
</style>

View file

@ -0,0 +1,25 @@
<script lang="ts" setup>
</script>
<template>
<form class="password-form">
<div class="form-group">
<label for="current-password">Current Password</label>
<input id="current-password" type="password"/>
</div>
<div class="form-group">
<label for="new-password">New Password</label>
<input id="new-password" type="password"/>
</div>
</form>
</template>
<style scoped>
.form-group {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View file

@ -0,0 +1,36 @@
<script lang="ts" setup>
</script>
<template>
<form class="profile-form">
<div class="form-group">
<label for="profile-first-name">First Name</label>
<input id="profile-first-name" type="text"/>
</div>
<div class="form-group">
<label for="profile-last-name">Last Name</label>
<input id="profile-last-name" type="text"/>
</div>
<div class="form-group">
<label for="profile-location">Location</label>
<input id="profile-location" type="text"/>
</div>
<div class="form-group">
<label for="profile-pic">Profile Picture</label>
<input id="profile-pic" type="file"/>
</div>
</form>
</template>
<style scoped>
.form-group {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

72
app/components/header.vue Normal file
View file

@ -0,0 +1,72 @@
<script lang="ts" setup>
</script>
<template>
<nav class="header">
<span class="logo">
<NuxtLink to="/">
Movie Night
</NuxtLink>
</span>
<ul class="links">
<li>
<NuxtLink to="/lists">Lists</NuxtLink>
</li>
<!-- <li>
<NuxtLink to="/schedule">Schedule</NuxtLink>
</li>
-->
<li>
<ProfileMenu/>
</li>
</ul>
</nav>
</template>
<style scoped>
.header {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
font: bold 1.5rem sans-serif;
justify-content: center;
margin-bottom: 2rem;
}
.links {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
list-style: none;
}
.links li {
cursor: pointer;
}
/* sm */
@media (min-width: 640px) {
.header {
justify-content: space-between;
flex-direction: row;
}
}
/* md */
@media (min-width: 768px) {
}
/* lg */
@media (min-width: 1024px) {
}
/* xl */
@media (min-width: 1280px) {
}
/* 2xl */
@media (min-width: 1536px) {
}
</style>

View file

@ -0,0 +1,136 @@
<script lang="ts" setup>
import type {List} from "~/types/list";
import {type ListSettings} from "~/types/list-settings";
const emit = defineEmits(['back-to-list'])
const props = defineProps<{
list: List
}>()
const listSettings: ListSettings = {
listName: 'My List',
isPublic: true,
collaborators: [
{id: 1, name: 'Ed', role: 3},
{id: 2, name: 'Bob', role: 2}
],
roles: [
{id: 1, name: 'Viewer'},
{id: 2, name: 'Editor'},
{id: 3, name: 'Admin'}
]
}
</script>
<template>
<div class="settings-header">
<div @click="$emit('back-to-list')">
<Icon name="solar:arrow-left-linear"/>
<span>Back to List</span>
</div>
</div>
<ul class="settings-list">
<li class="list-setting">
<label for="list-name-input">List Name</label>
<div>
<input id="list-name-input" :value="listSettings.listName" type="text"/>
<button>Save</button>
</div>
</li>
<li class="list-setting">
<div>
<label for="make-list-public">Make list public</label>
<input id="make-list-public" :checked="listSettings.isPublic" type="checkbox"/>
</div>
</li>
<li class="list-setting collaborator-list">
<span>Collaborators</span>
<details>
<summary>Permission levels</summary>
<ul>
<li>Viewer: Can view the list, but cannot make any changes.</li>
<li>Editor: Can add/remove movies from the list.</li>
<li>Admin: Can make any changes to the list including deleting it. Can also invite other users to collaborate
on
this list.
</li>
</ul>
</details>
<ul class="collaborators">
<li v-for="collaborator in listSettings.collaborators" :key="collaborator.id">
<span>{{ collaborator.name }}</span>
<select v-model="collaborator.role">
<option
v-for="role in listSettings.roles"
:value="role.id"
>
{{ role.name }}
</option>
</select>
</li>
</ul>
</li>
<li class="list-setting">
<label for="invite-collaborators-input">Invite Collaborators</label>
<textarea name="invite-collaborators-input" type="text"></textarea>
</li>
<li class="list-setting">
<label for="delete-list-button">Delete List</label>
<button name="delete-list-button">Delete</button>
</li>
</ul>
</template>
<style scoped>
.collaborator-list {
gap: 1rem;
}
.collaborators li {
display: flex;
justify-content: space-between;
}
.list-setting {
display: flex;
flex-direction: column;
gap: 1rem;
}
.settings-header {
display: flex;
justify-content: space-between;
padding: 1rem 0;
cursor: pointer;
}
.settings-header > div {
display: flex;
align-items: center;
}
.settings-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.settings-list > li {
display: flex;
border: gray 1px solid;
padding: 1rem;
}
details ul > li {
padding: 1rem;
border: black 1px solid;
}
details ul > li:not(:last-child) {
border-bottom: none;
}
</style>

View file

@ -0,0 +1,96 @@
<script lang="ts" setup>
import {type Movie} from "~/types/movie";
import posterPlaceholder from "~/assets/img/poster-placeholder.svg";
const props = defineProps<{
movies: Movie[]
}>()
const filteredMovies = ref<Movie[]>(props.movies);
const searchQuery = ref('');
const emit = defineEmits<{
'movie-clicked': [movie: Movie],
'add-movie': []
}>()
const movieSearch = () => {
filteredMovies.value = props.movies.filter(
movie => movie.title.toLowerCase().includes(searchQuery.value.toLowerCase())
);
}
</script>
<template>
<div>
<div class="list-controls-container">
<div class="list-controls">
<input v-model="searchQuery" placeholder="Search Movies" type="text" @keyup="movieSearch"/>
<Icon
class="list-controls-icon"
name="solar:filter-bold"
size="24"
title="Filter Movies"
/>
<Icon
class="list-controls-icon"
name="solar:sort-vertical-linear"
size="24"
title="Sort Movies"
/>
</div>
<button @click="emit('add-movie')">Add Movie</button>
</div>
<div v-if="filteredMovies.length === 0" class="movie-quote">
<span class="quote">"You complete me."</span>
<span class="attribution">Tom Cruise, Jerry Maguire</span>
</div>
<ul v-else class="movie-list">
<li v-for="movie in filteredMovies" :key="movie.id" class="movie" @click="emit('movie-clicked', movie)">
<img
alt=""
class="movie-poster"
src="http://fart.fart"
@error="(e) => (e.target as HTMLImageElement).src = posterPlaceholder"
/>
<span class="movie-title">{{ movie.title }}</span>
</li>
</ul>
</div>
</template>
<style scoped>
.list-controls-container {
display: flex;
justify-content: space-between;
gap: 1rem;
margin: 1rem 0;
}
.list-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.movie-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(max(140px, 20%), 1fr));
gap: 1rem;
}
.movie {
display: flex;
flex-direction: column;
cursor: pointer;
}
.movie-quote {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin: 5rem 0;
}
</style>

View file

@ -0,0 +1,38 @@
<script lang="ts" setup>
import type {Movie} from "~/types/movie";
defineProps<{
selectedMovie: Movie;
}>();
</script>
<template>
<div class="movie-details">
<img :alt="selectedMovie!.title" :src="selectedMovie!.poster" class="movie-poster"/>
<h2 class="movie-title">{{ selectedMovie!.title }}</h2>
</div>
</template>
<style scoped>
.movie-details {
display: flex;
flex-direction: column;
padding: 2rem;
}
.movie-poster {
width: 100%;
}
@media (max-width: 767px) {
.movie-details {
align-items: center;
}
.movie-poster {
margin: 0 auto;
}
}
</style>

View file

@ -0,0 +1,79 @@
<script lang="ts" setup>
const dropdownOpen = ref(false)
const profileMenu = ref<HTMLElement | null>(null)
function toggleDropdown() {
dropdownOpen.value = !dropdownOpen.value
}
function onClickOutside(e: MouseEvent) {
if (profileMenu.value && !profileMenu.value.contains(e.target as Node)) {
dropdownOpen.value = false
}
}
onMounted(() => document.addEventListener('click', onClickOutside))
onUnmounted(() => document.removeEventListener('click', onClickOutside))
</script>
<template>
<div ref="profileMenu" class="profile-menu">
<button class="profile-pic" @click="toggleDropdown">
<img alt="Profile" src="~/assets/img/profile-placeholder.svg"/>
</button>
<ul v-if="dropdownOpen" class="dropdown">
<li>
<NuxtLink to="/account" @click="dropdownOpen = false">Account</NuxtLink>
</li>
<li>Log Out</li>
</ul>
</div>
</template>
<style scoped>
.profile-menu {
position: relative;
}
.profile-pic {
background: none;
border: none;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
}
.profile-pic img {
width: 2rem;
height: 2rem;
border-radius: 50%;
}
.dropdown {
position: absolute;
right: 0;
top: 100%;
margin-top: 0.5rem;
background: white;
border: 1px solid #ccc;
border-radius: 0.5rem;
list-style: none;
padding: 0.25rem 0;
min-width: 8rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 10;
}
.dropdown li {
padding: 0.5rem 1rem;
font-size: 1rem;
white-space: nowrap;
}
.dropdown li:hover {
background: #f0f0f0;
}
</style>

View file

@ -0,0 +1,85 @@
<script lang="ts" setup>
defineProps<{
open: boolean
}>()
const emit = defineEmits<{
close: []
}>()
</script>
<template>
<Transition name="slideout-backdrop">
<div v-if="open" class="backdrop" @click="emit('close')"/>
</Transition>
<Transition name="slideout">
<div v-if="open" class="panel">
<button class="close-button" @click="emit('close')">&times;</button>
<slot/>
</div>
</Transition>
</template>
<style scoped>
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 100;
}
.panel {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 400px;
background: var(--color-surface, #fff);
z-index: 101;
overflow-y: auto;
padding: 1rem;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2);
}
.close-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: inherit;
line-height: 1;
padding: 0.25rem 0.5rem;
}
@media (max-width: 767px) {
.panel {
width: 100%;
}
}
/* Backdrop transitions */
.slideout-backdrop-enter-active,
.slideout-backdrop-leave-active {
transition: opacity 0.3s ease;
}
.slideout-backdrop-enter-from,
.slideout-backdrop-leave-to {
opacity: 0;
}
/* Panel transitions */
.slideout-enter-active,
.slideout-leave-active {
transition: transform 0.3s ease;
}
.slideout-enter-from,
.slideout-leave-to {
transform: translateX(100%);
}
</style>