initial commit
This commit is contained in:
commit
869be69d67
42 changed files with 11444 additions and 0 deletions
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
1
.nuxtrc
Normal file
1
.nuxtrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
setups.@nuxt/test-utils="4.0.0"
|
||||||
75
README.md
Normal file
75
README.md
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Nuxt Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm preview
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn preview
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
36
app/app.vue
Normal file
36
app/app.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NuxtRouteAnnouncer/>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage/>
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sm */
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* md */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* lg */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* xl */
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2xl */
|
||||||
|
@media (min-width: 1536px) {
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
app/assets/css/reset.css
Normal file
71
app/assets/css/reset.css
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/* Source: https://www.joshwcomeau.com/css/custom-css-reset */
|
||||||
|
|
||||||
|
/* 1. Use a more-intuitive box-sizing model */
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Remove default margin */
|
||||||
|
*:not(dialog) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Enable keyword animations */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
html {
|
||||||
|
interpolate-size: allow-keywords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* 4. Add accessible line-height */
|
||||||
|
line-height: 1.5;
|
||||||
|
/* 5. Improve text rendering */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6. Improve media defaults */
|
||||||
|
img, picture, video, canvas, svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 7. Inherit fonts for form controls */
|
||||||
|
input, button, textarea, select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 8. Avoid text overflows */
|
||||||
|
p, h1, h2, h3, h4, h5, h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 9. Improve line wrapping */
|
||||||
|
p {
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
10. Create a root stacking context
|
||||||
|
*/
|
||||||
|
#root, #__next {
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra CSS reset*/
|
||||||
|
|
||||||
|
/* Remove default list padding */
|
||||||
|
ul, ol {
|
||||||
|
padding-left: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default link styles */
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
4
app/assets/css/variables.css
Normal file
4
app/assets/css/variables.css
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
:root {
|
||||||
|
--color-primary: #000;
|
||||||
|
--color-surface: #fff;
|
||||||
|
}
|
||||||
48
app/assets/img/poster-placeholder.svg
Normal file
48
app/assets/img/poster-placeholder.svg
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="1413.5 192.80002 108.59998 149.10001"
|
||||||
|
width="108.59998"
|
||||||
|
height="149.10001"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="poster-placeholder.svg"
|
||||||
|
inkscape:export-filename="poster-placeholder2.svg"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:export-bgcolor="#ffffffff">
|
||||||
|
<inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="108.59998"
|
||||||
|
height="149.10001"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<rect
|
||||||
|
style="fill:#5c5b79;fill-opacity:1"
|
||||||
|
id="rect1"
|
||||||
|
width="108.28388"
|
||||||
|
height="148.72725"
|
||||||
|
x="1413.6864"
|
||||||
|
y="193.17278" />
|
||||||
|
<path
|
||||||
|
fill="#cccccc"
|
||||||
|
d="m 1413.5,242.5 c 0,16.6 0,33.1 0,49.7 36.2,49.7 72.4,-49.7 108.6,0 0,-16.6 0,-33.1 0,-49.7 -36.2,-49.7 -72.4,49.7 -108.6,0 z m 8.7,56.7 c -1.6,-1.3 -3.3,-2.8 -4.9,-4.6 0,-1.6 0,-3.3 0,-4.9 1.6,1.8 3.3,3.3 4.9,4.6 0,1.7 0,3.3 0,4.9 z m 0,-39.8 c -1.6,-1.3 -3.3,-2.8 -4.9,-4.6 0,-1.6 0,-3.3 0,-4.9 1.6,1.8 3.3,3.3 4.9,4.6 0,1.6 0,3.3 0,4.9 z m 9.6,44.4 c -1.6,-0.4 -3.3,-0.9 -4.9,-1.7 0,-1.6 0,-3.3 0,-4.9 1.6,0.8 3.3,1.4 4.9,1.7 0,1.7 0,3.3 0,4.9 z m 0,-39.8 c -1.6,-0.4 -3.3,-0.9 -4.9,-1.7 0,-1.6 0,-3.3 0,-4.9 1.6,0.8 3.3,1.4 4.9,1.7 0,1.7 0,3.3 0,4.9 z m 9.6,39.8 c -1.6,0.3 -3.3,0.5 -4.9,0.5 0,-1.6 0,-3.3 0,-4.9 1.6,0 3.3,-0.2 4.9,-0.5 0,1.7 0,3.3 0,4.9 z m 0,-39.8 c -1.6,0.3 -3.3,0.5 -4.9,0.5 0,-1.6 0,-3.3 0,-4.9 1.6,0 3.3,-0.2 4.9,-0.5 0,1.6 0,3.3 0,4.9 z m 9.6,36.4 c -1.6,0.8 -3.3,1.5 -4.9,2.1 0,-1.6 0,-3.3 0,-4.9 1.6,-0.6 3.3,-1.3 4.9,-2.1 0,1.7 0,3.3 0,4.9 z m 0,-39.8 c -1.6,0.8 -3.3,1.5 -4.9,2.1 0,-1.6 0,-3.3 0,-4.9 1.6,-0.6 3.3,-1.3 4.9,-2.1 0,1.6 0,3.3 0,4.9 z m 9.5,34.3 c -1.6,1.1 -3.3,2.1 -4.9,3 0,-1.6 0,-3.3 0,-4.9 1.6,-1 3.3,-2 4.9,-3 0,1.6 0,3.2 0,4.9 z m 0,-39.8 c -1.6,1.1 -3.3,2.1 -4.9,3 0,-1.6 0,-3.3 0,-4.9 1.6,-1 3.3,-2 4.9,-3 0,1.6 0,3.2 0,4.9 z m 9.6,33.3 c -1.6,1.1 -3.3,2.3 -4.9,3.4 0,-1.6 0,-3.3 0,-4.9 1.6,-1.1 3.3,-2.2 4.9,-3.4 0,1.6 0,3.2 0,4.9 z m 0,-39.8 c -1.6,1.1 -3.3,2.3 -4.9,3.4 0,-1.6 0,-3.3 0,-4.9 1.6,-1.1 3.3,-2.2 4.9,-3.4 0,1.6 0,3.2 0,4.9 z m 9.6,33.6 c -1.6,1 -3.3,2 -4.9,3.1 0,-1.6 0,-3.3 0,-4.9 1.6,-1.1 3.3,-2.1 4.9,-3.1 0,1.6 0,3.3 0,4.9 z m 0,-39.8 c -1.6,1 -3.3,2 -4.9,3.1 0,-1.6 0,-3.3 0,-4.9 1.6,-1.1 3.3,-2.1 4.9,-3.1 0,1.6 0,3.2 0,4.9 z m 9.6,35.1 c -1.6,0.6 -3.3,1.3 -4.9,2.1 0,-1.6 0,-3.3 0,-4.9 1.6,-0.8 3.3,-1.5 4.9,-2.1 0,1.7 0,3.3 0,4.9 z m 0,-39.8 c -1.6,0.6 -3.3,1.3 -4.9,2.1 0,-1.6 0,-3.3 0,-4.9 1.6,-0.8 3.3,-1.5 4.9,-2.1 0,1.7 0,3.3 0,4.9 z m 9.6,37.9 c -1.6,0 -3.3,0.2 -4.9,0.6 0,-1.6 0,-3.3 0,-4.9 1.6,-0.3 3.3,-0.5 4.9,-0.6 0,1.7 0,3.3 0,4.9 z m 0,-39.8 c -1.6,0 -3.3,0.2 -4.9,0.6 0,-1.6 0,-3.3 0,-4.9 1.6,-0.3 3.3,-0.5 4.9,-0.6 0,1.6 0,3.3 0,4.9 z m 14.2,0 c 1.6,1.2 3.3,2.7 4.9,4.5 0,1.6 0,3.3 0,4.9 -1.6,-1.8 -3.3,-3.2 -4.9,-4.5 0,-1.6 0,-3.2 0,-4.9 z m 0,39.8 c 1.6,1.2 3.3,2.7 4.9,4.5 0,1.6 0,3.3 0,4.9 -1.6,-1.8 -3.3,-3.2 -4.9,-4.5 0,-1.6 0,-3.2 0,-4.9 z m -9.6,-44.2 c 1.6,0.3 3.3,0.9 4.9,1.6 0,1.6 0,3.3 0,4.9 -1.6,-0.8 -3.3,-1.3 -4.9,-1.6 0,-1.7 0,-3.3 0,-4.9 z m 0,39.8 c 1.6,0.3 3.3,0.9 4.9,1.6 0,1.6 0,3.3 0,4.9 -1.6,-0.8 -3.3,-1.3 -4.9,-1.6 0,-1.7 0,-3.3 0,-4.9 z"
|
||||||
|
id="path1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
5
app/assets/img/profile-placeholder.svg
Normal file
5
app/assets/img/profile-placeholder.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<circle cx="32" cy="32" r="32" fill="#ccc"/>
|
||||||
|
<circle cx="32" cy="24" r="10" fill="#fff"/>
|
||||||
|
<path d="M12 56c0-11 9-20 20-20s20 9 20 20" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 233 B |
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>
|
||||||
19
app/layouts/default.vue
Normal file
19
app/layouts/default.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<Header/>
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 5rem;
|
||||||
|
max-width: 1280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
54
app/pages/account.vue
Normal file
54
app/pages/account.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import PageTitle from "~/components/common/page-title.vue";
|
||||||
|
import PasswordResetForm from "~/components/forms/password-reset-form.vue";
|
||||||
|
import ProfileForm from "~/components/forms/profile-form.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageTitle title="Account Settings"/>
|
||||||
|
|
||||||
|
<div class="password-settings settings-section">
|
||||||
|
<h2>Reset Password</h2>
|
||||||
|
<PasswordResetForm/>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-settings settings-section">
|
||||||
|
<div class="profile-header">
|
||||||
|
<h2>Profile</h2>
|
||||||
|
<span class="public-profile-link">View Public Profile</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProfileForm/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-profile-link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
39
app/pages/index.vue
Normal file
39
app/pages/index.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import PageTitle from "~/components/common/page-title.vue";
|
||||||
|
|
||||||
|
const user = "Eddie";
|
||||||
|
const welcomeMessage = computed(() => `Welcome, ${user}!`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageTitle :title="welcomeMessage"/>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div>
|
||||||
|
<h2>Next up</h2>
|
||||||
|
<span>Nothing Scheduled :(</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Activity Feed</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Ed created a list: "Best of 2023"</li>
|
||||||
|
<li>Ed added a movie to "Best of 2023": "The Dark Knight"</li>
|
||||||
|
<li>Ed added a movie to "Best of 2023": "The Dark Knight Rises"</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-title {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
70
app/pages/lists/[id].vue
Normal file
70
app/pages/lists/[id].vue
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import PageTitle from "~/components/common/page-title.vue";
|
||||||
|
import {type Movie} from "~/types/movie";
|
||||||
|
import {type List} from "~/types/list";
|
||||||
|
import MovieDetails from "~/components/panels/movie-details.vue";
|
||||||
|
|
||||||
|
const settingsActive = ref(false);
|
||||||
|
const movieSearchActive = ref(false);
|
||||||
|
const toggleSettings = () => settingsActive.value = !settingsActive.value
|
||||||
|
const toggleMovieSearch = () => movieSearchActive.value = !movieSearchActive.value
|
||||||
|
|
||||||
|
const selectedMovie = ref<Movie | null>(null);
|
||||||
|
|
||||||
|
const list: List = {
|
||||||
|
id: 1,
|
||||||
|
name: 'List Name',
|
||||||
|
isPublic: true,
|
||||||
|
listSettings: {
|
||||||
|
listName: 'List Name',
|
||||||
|
isPublic: true,
|
||||||
|
collaborators: [],
|
||||||
|
roles: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const movies: Movie[] = []
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-header">
|
||||||
|
<PageTitle title="List Name"/>
|
||||||
|
<Icon class="settings-icon" name="solar:settings-bold" @click="toggleSettings"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ListSettings
|
||||||
|
v-if="settingsActive"
|
||||||
|
:list="list"
|
||||||
|
v-on:back-to-list="toggleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MovieList
|
||||||
|
v-else
|
||||||
|
:movies="movies"
|
||||||
|
@movie-clicked="selectedMovie = $event"
|
||||||
|
@add-movie="toggleMovieSearch"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SlideoutPanel :open="!!selectedMovie" @close="selectedMovie = null">
|
||||||
|
<MovieDetails v-if="selectedMovie" :selectedMovie="selectedMovie"/>
|
||||||
|
</SlideoutPanel>
|
||||||
|
|
||||||
|
<SlideoutPanel :open="movieSearchActive" class="movie-search-panel"
|
||||||
|
@close="movieSearchActive = false">
|
||||||
|
<p>Movie Search</p>
|
||||||
|
</SlideoutPanel>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
41
app/pages/lists/index.vue
Normal file
41
app/pages/lists/index.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import PageTitle from "~/components/common/page-title.vue";
|
||||||
|
import CreateListForm from "~/components/forms/create-list-form.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageTitle title="Lists"/>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<CreateListForm/>
|
||||||
|
|
||||||
|
<div class="w-full flex flex-col gap-5">
|
||||||
|
<h2 class="text-2xl font-bold">Your Lists</h2>
|
||||||
|
<ul class="w-full flex flex-col gap-3">
|
||||||
|
<li class="flex justify-between items-center p-4 bg-gray-700/50 rounded-lg hover:bg-gray-600/50 transition-colors">
|
||||||
|
<NuxtLink to="lists/1">List 1</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full flex flex-col gap-5">
|
||||||
|
<h2 class="text-2xl font-bold">Shared With You</h2>
|
||||||
|
<ul class="w-full flex flex-col gap-3">
|
||||||
|
<li class="flex justify-between items-center p-4 bg-gray-700/50 rounded-lg hover:bg-gray-600/50 transition-colors">
|
||||||
|
<NuxtLink to="lists/2">Bob's List</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
6
app/types/collaborator.ts
Normal file
6
app/types/collaborator.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export type Collaborator = {
|
||||||
|
id: number,
|
||||||
|
name: string
|
||||||
|
role: number
|
||||||
|
}
|
||||||
|
|
||||||
9
app/types/list-settings.ts
Normal file
9
app/types/list-settings.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type {Collaborator} from "~/types/collaborator";
|
||||||
|
import type {Role} from "~/types/role";
|
||||||
|
|
||||||
|
export type ListSettings = {
|
||||||
|
listName: string,
|
||||||
|
isPublic: boolean,
|
||||||
|
collaborators: Collaborator[],
|
||||||
|
roles: Role[]
|
||||||
|
}
|
||||||
9
app/types/list.ts
Normal file
9
app/types/list.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {type ListSettings} from "~/types/list-settings";
|
||||||
|
|
||||||
|
export type List = {
|
||||||
|
id: number,
|
||||||
|
name: string
|
||||||
|
isPublic: boolean
|
||||||
|
listSettings: ListSettings
|
||||||
|
}
|
||||||
|
|
||||||
14
app/types/movie.ts
Normal file
14
app/types/movie.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export type Movie = {
|
||||||
|
id: number,
|
||||||
|
title: string
|
||||||
|
imdb_id: string
|
||||||
|
director: string
|
||||||
|
actors: string
|
||||||
|
plot: string
|
||||||
|
genre: string
|
||||||
|
mpaa_rating: string
|
||||||
|
critic_scores: string
|
||||||
|
poster: string
|
||||||
|
added_by: number
|
||||||
|
}
|
||||||
|
|
||||||
5
app/types/role.ts
Normal file
5
app/types/role.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type Role = {
|
||||||
|
id: number,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
6
eslint.config.mjs
Normal file
6
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// @ts-check
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt(
|
||||||
|
// Your custom configs here
|
||||||
|
)
|
||||||
13
nuxt.config.ts
Normal file
13
nuxt.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2025-07-15',
|
||||||
|
devtools: {enabled: true},
|
||||||
|
css: ['~/assets/css/reset.css'],
|
||||||
|
modules: [
|
||||||
|
'@nuxt/eslint',
|
||||||
|
'@nuxt/fonts',
|
||||||
|
'@nuxt/icon',
|
||||||
|
'@nuxt/image',
|
||||||
|
'@nuxt/test-utils'
|
||||||
|
]
|
||||||
|
})
|
||||||
37
package.json
Normal file
37
package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "movie-night-web",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:watch": "vitest --watch",
|
||||||
|
"test:coverage": "vitest --coverage",
|
||||||
|
"test:unit": "vitest --project unit",
|
||||||
|
"test:nuxt": "vitest --project nuxt",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/eslint": "1.15.1",
|
||||||
|
"@nuxt/fonts": "0.13.0",
|
||||||
|
"@nuxt/icon": "2.2.1",
|
||||||
|
"@nuxt/image": "2.0.0",
|
||||||
|
"@nuxt/test-utils": "4.0.0",
|
||||||
|
"nuxt": "^4.3.1",
|
||||||
|
"vue": "^3.5.28",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
|
"happy-dom": "^20.6.1",
|
||||||
|
"playwright-core": "^1.58.2",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
playwright.config.ts
Normal file
24
playwright.config.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
import type { ConfigOptions } from '@nuxt/test-utils/playwright'
|
||||||
|
|
||||||
|
export default defineConfig<ConfigOptions>({
|
||||||
|
testDir: './tests',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
nuxt: {
|
||||||
|
rootDir: fileURLToPath(new URL('.', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
10110
pnpm-lock.yaml
generated
Normal file
10110
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- '@parcel/watcher'
|
||||||
|
- esbuild
|
||||||
|
- sharp
|
||||||
|
- unrs-resolver
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
17
test/nuxt/component.test.ts
Normal file
17
test/nuxt/component.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||||
|
import { defineComponent, h } from 'vue'
|
||||||
|
|
||||||
|
describe('component test example', () => {
|
||||||
|
it('can mount components', async () => {
|
||||||
|
const TestComponent = defineComponent({
|
||||||
|
setup() {
|
||||||
|
return () => h('div', 'Hello Nuxt!')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const component = await mountSuspended(TestComponent)
|
||||||
|
|
||||||
|
expect(component.text()).toBe('Hello Nuxt!')
|
||||||
|
})
|
||||||
|
})
|
||||||
7
test/unit/example.test.ts
Normal file
7
test/unit/example.test.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
describe('example unit test', () => {
|
||||||
|
it('should pass', () => {
|
||||||
|
expect(1 + 1).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
6
tests/example.spec.ts
Normal file
6
tests/example.spec.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { expect, test } from '@nuxt/test-utils/playwright'
|
||||||
|
|
||||||
|
test('example e2e test', async ({ page, goto }) => {
|
||||||
|
await goto('/', { waitUntil: 'hydration' })
|
||||||
|
await expect(page).toHaveTitle(/Nuxt/)
|
||||||
|
})
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
vitest.config.ts
Normal file
34
vitest.config.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import { defineVitestProject } from '@nuxt/test-utils/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
test: {
|
||||||
|
name: 'unit',
|
||||||
|
include: ['test/unit/*.{test,spec}.ts'],
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
await defineVitestProject({
|
||||||
|
test: {
|
||||||
|
name: 'nuxt',
|
||||||
|
include: ['test/nuxt/*.{test,spec}.ts'],
|
||||||
|
environment: 'nuxt',
|
||||||
|
environmentOptions: {
|
||||||
|
nuxt: {
|
||||||
|
rootDir: fileURLToPath(new URL('.', import.meta.url)),
|
||||||
|
domEnvironment: 'happy-dom',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
coverage: {
|
||||||
|
enabled: true,
|
||||||
|
provider: 'v8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue