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