initial commit
This commit is contained in:
commit
869be69d67
42 changed files with 11444 additions and 0 deletions
11
app/components/common/combo-box.vue
Normal file
11
app/components/common/combo-box.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
18
app/components/common/page-title.vue
Normal file
18
app/components/common/page-title.vue
Normal 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>
|
||||
32
app/components/forms/create-list-form.vue
Normal file
32
app/components/forms/create-list-form.vue
Normal 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>
|
||||
25
app/components/forms/password-reset-form.vue
Normal file
25
app/components/forms/password-reset-form.vue
Normal 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>
|
||||
36
app/components/forms/profile-form.vue
Normal file
36
app/components/forms/profile-form.vue
Normal 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
72
app/components/header.vue
Normal 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>
|
||||
136
app/components/list-settings.vue
Normal file
136
app/components/list-settings.vue
Normal 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>
|
||||
96
app/components/movie-list.vue
Normal file
96
app/components/movie-list.vue
Normal 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>
|
||||
38
app/components/panels/movie-details.vue
Normal file
38
app/components/panels/movie-details.vue
Normal 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>
|
||||
79
app/components/profile-menu.vue
Normal file
79
app/components/profile-menu.vue
Normal 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>
|
||||
85
app/components/slideout-panel.vue
Normal file
85
app/components/slideout-panel.vue
Normal 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')">×</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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue