Compare commits

..

No commits in common. "main" and "showings-updates" have entirely different histories.

44 changed files with 3171 additions and 6891 deletions

2
.gitignore vendored
View file

@ -6,5 +6,3 @@ node_modules
.output
.env
dist
.idea

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/web.iml" filepath="$PROJECT_DIR$/.idea/web.iml" />
</modules>
</component>
</project>

33
.idea/php.xml generated Normal file
View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="53f7f9d8-dddf-4d4f-ad7e-428c57c55670" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpStan">
<PhpStan_settings>
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="53f7f9d8-dddf-4d4f-ad7e-428c57c55670" timeout="60000" />
</PhpStan_settings>
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="Psalm">
<Psalm_settings>
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="53f7f9d8-dddf-4d4f-ad7e-428c57c55670" timeout="60000" />
</Psalm_settings>
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

7
.idea/prettier.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

8
.idea/web.iml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

150
.idea/workspace.xml generated Normal file
View file

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="5e320804-68c9-4504-97d5-d421de3438b2" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/admin/search.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/admin/search.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/admin/showings.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/admin/showings.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/types/showing.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/showing.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ComposerSettings">
<execution />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="CSS File" />
<option value="Vue Single File Component" />
<option value="TypeScript File" />
</list>
</option>
</component>
<component name="FormatOnSaveOptions">
<option name="myRunOnSave" value="true" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MacroExpansionManager">
<option name="directoryName" value="cvh2jllu" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="OptimizeOnSaveOptions">
<option name="myRunOnSave" value="true" />
</component>
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="/usr/bin/php" />
<component name="ProblemsViewState">
<option name="selectedTabId" value="CurrentFile" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 8
}</component>
<component name="ProjectId" id="2K1xoABBc3o8XBi4orGtOrB9Dxn" />
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="1" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
&quot;code.cleanup.on.save&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;showings-updates&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/tiradoe/Projects/movie-night/web/src/types&quot;,
&quot;list.type.of.created.stylesheet&quot;: &quot;CSS&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.standard&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.standard&quot;: &quot;&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;npm.dev.executor&quot;: &quot;Run&quot;,
&quot;prettierjs.PrettierConfiguration.Package&quot;: &quot;/home/tiradoe/Projects/movie-night/web/src/node_modules/prettier&quot;,
&quot;rearrange.code.on.save&quot;: &quot;true&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;settings.vue&quot;,
&quot;ts.external.directory.path&quot;: &quot;/home/tiradoe/Projects/movie-night/web/src/node_modules/typescript/lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/types" />
<recent name="$PROJECT_DIR$/components" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/public" />
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/components/modal-content" />
<recent name="$PROJECT_DIR$/assets/css" />
</key>
</component>
<component name="RunManager">
<configuration name="dev" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/src/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-1632447f56bf-JavaScript-PS-243.26053.13" />
<option value="bundled-php-predefined-a98d8de5180a-1ec7b7818973-com.jetbrains.php.sharedIndexes-PS-243.26053.13" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="5e320804-68c9-4504-97d5-d421de3438b2" name="Changes" comment="" />
<created>1673156065550</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1673156065550</updated>
<workItem from="1673156066556" duration="5928000" />
<workItem from="1673164832861" duration="23180000" />
<workItem from="1673308542228" duration="9499000" />
<workItem from="1673325248718" duration="9321000" />
<workItem from="1673334572599" duration="6831000" />
<workItem from="1673385927574" duration="3502000" />
<workItem from="1673397538637" duration="47416000" />
<workItem from="1673508732689" duration="1316000" />
<workItem from="1673547794038" duration="2346000" />
<workItem from="1743904898331" duration="20256000" />
<workItem from="1743998844137" duration="23268000" />
<workItem from="1744430699183" duration="32152000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="UnknownFeatures">
<option featureType="com.intellij.fileTypeFactory" implementationName=".env" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
</project>

View file

@ -2,5 +2,3 @@ Movie Night Web
=
![cinema corona list page](screenshot.png "Screenshot")
Uses filmstrip icon by momentbloom (https://www.vecteezy.com/vector-art/36659391-filmstrip)

View file

@ -94,12 +94,6 @@ input {
width: 80%; /* Could be more or less, depending on screen size */
}
.page-header {
font-size: 1.5rem;
line-height: calc(2 / 1.5);
padding-bottom: 1rem;
}
.hover-pointer {
cursor: pointer;
}

Binary file not shown.

Binary file not shown.

View file

@ -1,48 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,30 +0,0 @@
<template>
<div
class="flex absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex-col"
>
<video alt="Loading" autoplay class="max-h-52" loop muted playsinline>
<source
v-if="supportsHEVC"
src="/assets/img/movie-loader.mov"
type="video/quicktime"
/>
<source v-else src="/assets/img/movie-loader.webm" type="video/webm" />
</video>
</div>
</template>
<script lang="ts" setup>
const supportsHEVC = ref(false);
onMounted(() => {
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
// iOS devices also support HEVC
const isIOS =
/iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
supportsHEVC.value = isSafari || isIOS;
});
</script>
<style scoped></style>

23
src/components/Modal.vue Normal file
View file

@ -0,0 +1,23 @@
<template>
<div v-show="visible" id="movie-modal" class="movie-modal movie-card p-5">
<span
class="hover-pointer font-bold w-full block text-right sm:pr-5 pb-3 pt-5"
@click="toggleModal()"
>
X
</span>
<slot class=""></slot>
</div>
</template>
<script lang="ts" setup>
const visible = ref(false);
const toggleModal = () => {
visible.value = !visible.value;
};
defineExpose({ toggleModal });
</script>
<style scoped></style>

View file

@ -1,36 +0,0 @@
<script lang="ts" setup>
import placeholderPoster from "assets/img/poster-placeholder.svg";
const imgRef = ref<HTMLImageElement | null>(null);
const props = defineProps<{ image: string }>();
const handleImageError = function (event: Event) {
(event.target as HTMLImageElement).classList.remove("lazyload");
(event.target as HTMLImageElement).classList.add("object-cover");
(event.target as HTMLImageElement).src = placeholderPoster;
};
watch(
() => props.image,
(newImage) => {
if (imgRef.value && newImage) {
imgRef.value.classList.add("lazyload");
imgRef.value.setAttribute("data-src", newImage);
}
},
);
</script>
<template>
<div class="aspect-[2/3] w-full text-blue-300">
<img
ref="imgRef"
:data-src="props.image"
alt="Movie Details"
class="lazyload hover-pointer w-full h-full"
@error="handleImageError"
/>
</div>
</template>
<style scoped></style>

View file

@ -1,54 +0,0 @@
<template>
<blockquote
class="flex flex-col gap-10 movie-card p-10 neon-border text-center"
>
<span>
{{ quote.quote }}
</span>
<div class="flex gap-2 italic flex-col">
<span>{{ quote.actor }}</span>
<span>{{ quote.movie }}</span>
</div>
</blockquote>
</template>
<style scoped></style>
<script lang="ts" setup>
const quotes = [
{
actor: "Darren Ewing as Arnold",
movie: "Troll 2",
quote: "They're eating her... and then they're going to eat me! OH MY GOD!",
},
{
actor: "Tommy Wiseau as Johnny",
movie: "The Room",
quote: "You're tearing me apart, Lisa!",
},
{
actor: "Arnold Schwaarzenegger as T-800",
movie: "Terminator 2: Judgment Day",
quote: "Hasta la vista, baby.",
},
{
actor: "Karolyn Grimes as Zuzu",
movie: "It's a Wonderful Life",
quote: "Every time a bell rings, an angel gets his wings.",
},
{
actor: "Pat Morita as Mr. Miyagi",
movie: "The Karate Kid",
quote: "Wax on, Wax off",
},
{
actor: "Dolph Lundgren as Drago",
movie: "Rocky 4",
quote: "I must break you.",
},
];
const randomQuote = () => quotes[Math.floor(Math.random() * quotes.length)];
const quote = ref(randomQuote());
</script>

View file

@ -3,101 +3,90 @@
<div id="add-list-container" class="my-5">
<label class="text-md font-bold" for="add-list">Add List</label>
<div class="flex">
<input
id="add-list"
class="p-1 rounded-l"
placeholder="List Title"
type="text"
v-on:keyup.enter="addList"
/>
<input id="add-list" class="p-1 rounded-l" placeholder="List Title" type="text" v-on:keyup.enter="addList"/>
<button class="btn p-1 rounded-r" @click="addList">Add</button>
</div>
</div>
<ul class="grid grid-rows gap-2">
<li v-for="list in lists" class="movie-card p-3 neon-border">
<span class="mb-2">{{ list.name }}</span> <br />
<button
class="btn mt-2 p-1 rounded"
type="button"
@click="deleteList(list.id)"
>
Delete
</button>
<span class="mb-2">{{ list.name }}</span> <br/>
<button class="btn mt-2 p-1 rounded" type="button" @click="deleteList(list.id)">Delete</button>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import type { MovieList } from "~/types/movielist";
<script>
export default {
name: "lists",
data: () => ({
lists: [],
}),
methods: {
addList: async function () {
let config = useRuntimeConfig();
const list_name = document.getElementById("add-list").value;
if (!list_name) {
alert("Please add list name.");
return
}
let list_json = await fetch(`${config.public.apiURL}/lists`, {
method: "POST",
body: JSON.stringify({
name: list_name,
public: false
}),
headers: {
"Content-type": "application/json",
"token": useCookie("token").value,
}
})
.then(response => response.json())
.then(json => json)
.catch(err => console.log(err))
const lists = ref<MovieList[]>([]);
list_json.list.movie_count = 0;
this.lists.push(list_json.list);
},
deleteList: function (list_id) {
const config = useRuntimeConfig();
let confirmed = confirm("Delete list?");
const addList = async function () {
let config = useRuntimeConfig();
let list_name = (document.getElementById("add-list") as HTMLInputElement)
.value;
if (!confirmed) {
return false;
}
if (!list_name) {
alert("Please add list name.");
return;
return fetch(`${config.public.apiURL}/lists/${list_id}`, {
credentials: "include",
method: "DELETE",
headers: {
"Content-type": "application/json",
"token": useCookie("token").value,
}
})
.then(response => response.json())
.then(_json => {
window.location.reload();
});
},
getLists: function () {
const config = useRuntimeConfig();
fetch(`${config.public.apiURL}/lists`, {
method: "GET",
headers: {"Content-type": "application/json"}
})
.then(response => response.json())
.then(json => this.lists = json)
.catch(err => console.log(err))
},
},
mounted() {
this.getLists();
}
await $fetch<MovieList>(`${config.public.apiURL}/lists/`, {
method: "POST",
body: JSON.stringify({
name: list_name,
public: false,
}),
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
})
.then((data) => {
lists.value = [...lists.value, data];
(document.getElementById("add-list") as HTMLInputElement).value = "";
})
.catch((err) => console.log(err));
};
const deleteList = function (list_id: number) {
const config = useRuntimeConfig();
let confirmed = confirm("Delete list?");
if (!confirmed) {
return false;
}
$fetch(`${config.public.apiURL}/lists/${list_id}/`, {
method: "DELETE",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
})
.then(() => {
lists.value = lists.value.filter((list) => {
return list.id !== list_id;
});
})
.catch((err) => console.log(err));
};
const getLists = function () {
const config = useRuntimeConfig();
$fetch<MovieList[]>(`${config.public.apiURL}/lists`, {
method: "GET",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
})
.then((data) => (lists.value = data))
.catch((err) => console.log(err));
};
onMounted(() => {
getLists();
});
}
</script>
<style scoped></style>
<style scoped>
</style>

View file

@ -1,5 +1,4 @@
<template>
<LoadingIcon v-if="loading" class="p-1 bg-gray-900 rounded-3xl" />
<form class="py-3 p-sm-0 align-items-center" @submit="findMovies">
<label class="px-0" for="search-field">Search</label>
<div class="px-0 mx-0">
@ -9,7 +8,12 @@
</form>
<ul class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-4">
<li v-for="movie in movies" class="p-1 movie-card">
<MoviePoster :image="movie.poster" @click="showModal(movie)" />
<img
:src="movie.poster"
alt="movie poster"
class="neon-border hover-pointer"
@click="showModal(movie)"
/>
<div class="p-2">
<h5 class="text-center">{{ movie.title }} ({{ movie.year }})</h5>
</div>
@ -19,14 +23,11 @@
<script lang="ts" setup>
import type { Movie } from "~/types/movie";
import "lazysizes";
const loading = ref(false);
const emit = defineEmits<{
(e: "show-modal", movie: Movie): void;
}>();
const movies = ref<Movie[]>([]);
const movies = defineModel<Movie[]>("movie_list", { default: [] });
const showModal = (movie: Movie) => {
emit("show-modal", movie);
@ -35,7 +36,6 @@ const showModal = (movie: Movie) => {
const findMovies = async function (e: Event) {
let config = useRuntimeConfig();
e.preventDefault();
loading.value = true;
let searchTerm = (document.getElementById("search-field") as HTMLInputElement)
?.value;
@ -43,27 +43,28 @@ const findMovies = async function (e: Event) {
return;
}
$fetch<Movie[]>(`${config.public.apiURL}/movies/search?q=${searchTerm}`, {
method: "GET",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
const { data, error } = await useFetch<Movie[]>(
`${config.public.apiURL}/movies/search?q=${searchTerm}`,
{
method: "GET",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
},
})
.then((data) => {
movies.value = data;
loading.value = false;
})
.catch((err) => {
if (err.statusCode === 401) {
navigateTo("/login");
} else if (err.statusCode === 404) {
alert("No movies found");
loading.value = false;
} else {
alert("An error occurred. Please try again later.");
}
});
);
if (error.value) {
if (error.value.statusCode === 401) {
alert("Unauthorized");
}
} else {
if (!data.value) {
alert("No movies found.");
} else {
movies.value = data.value || [];
}
}
};
</script>

View file

@ -26,11 +26,26 @@
import type { Showing } from "~/types/showing";
import { useCookie } from "#app";
import type { Schedule } from "~/types/schedule";
import { $fetch } from "ofetch";
const showings = ref<Showing[]>([]);
const previous_showings = ref<Showing[]>([]);
const showings = defineModel<Showing[]>("showings", { default: [] });
const previous_showings = defineModel<Showing[]>("previous_showings", {
default: [],
});
const got_previous = ref(false);
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
onMounted(() => {
getShowings();
@ -38,20 +53,6 @@ onMounted(() => {
const formatDate = function (date_string: string) {
let parsed_date = new Date(Date.parse(date_string));
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
let month = months[parsed_date.getMonth()];
return `${month} ${parsed_date.getDate()}, ${parsed_date.getFullYear()}`;
@ -85,27 +86,33 @@ const getShowings = function (previous = false) {
let params = "";
if (previous) params = "?previous=true";
$fetch<Schedule>(`${config.public.apiURL}/schedules/1${params}`, {
method: "GET",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
const { data, error } = useFetch<Schedule>(
`${config.public.apiURL}/schedules/1${params}`,
{
method: "GET",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
},
})
.then((data) => {
);
if (error.value) {
if (error.value.statusCode === 401) {
alert("Unauthorized");
}
} else {
if (!data.value) {
alert("No showings found for schedule.");
} else {
if (previous) {
got_previous.value = true;
previous_showings.value = data.showings;
previous_showings.value = data.value.showings;
} else {
showings.value = data.showings;
showings.value = data.value.showings;
}
})
.catch((err) => {
if (err.statusCode === 401) {
useCookie("token").value = null;
navigateTo("/");
}
});
}
}
};
</script>

View file

@ -1,120 +0,0 @@
<script lang="ts" setup>
import { logout } from "~/composables/logout";
let isOpened = ref(false);
const menuRef = ref<HTMLElement>();
const toggleMenu = function () {
isOpened.value = !isOpened.value;
};
onMounted(() => {
const handleClickOutside = (e: Event) => {
if (!menuRef.value?.contains(e.target as Node)) {
isOpened.value = false;
} else {
const target = e.target as HTMLElement;
if (
target.classList.contains("menu-link") ||
target.closest(".menu-link")
) {
isOpened.value = false;
}
}
};
document.addEventListener("click", handleClickOutside);
onUnmounted(() => {
document.removeEventListener("click", handleClickOutside);
});
});
</script>
<template>
<div ref="menuRef" class="profile-menu">
<img
alt="profile menu"
class="profile-pic"
src="https://placecage.lucidinternets.com/50/50"
tabindex="0"
@click="toggleMenu"
@keydown.enter="toggleMenu"
@keydown.space="toggleMenu"
/>
<div class="menu-content">
<ul v-show="isOpened">
<li role="none">
<NuxtLink class="menu-link" to="/admin">Admin</NuxtLink>
</li>
<li role="none">
<NuxtLink class="menu-link" to="/user/profile"> Profile</NuxtLink>
</li>
<!--
<li role="none">
<NuxtLink class="menu-link" to="/user/settings"> Settings</NuxtLink>
</li>
-->
<li
id="logout"
class="menu-link"
role="none"
tabindex="0"
@click="logout"
>
Logout
</li>
</ul>
</div>
</div>
</template>
<style scoped>
.profile-menu {
position: relative;
display: inline-block;
}
.profile-pic {
width: 2rem;
height: 2rem;
border-radius: 50%;
margin-right: 0.5rem;
}
.profile-pic:hover {
border: 1px solid #6f0b51;
}
.menu-content {
position: absolute;
top: 100%;
right: 0;
min-width: 150px;
background-color: #f9f9f9;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
z-index: 1;
overflow: hidden;
border-radius: 0.5rem;
text-align: center;
}
.menu-content li {
color: black;
text-decoration: none;
display: block;
}
.menu-content li:hover {
background-color: #6f0b51;
color: white;
}
.menu-link {
padding: 12px 16px;
width: 100%;
height: 100%;
display: block;
cursor: pointer;
}
</style>

View file

@ -1,45 +0,0 @@
<script lang="ts" setup>
const showScroll = ref(false);
const updateScrollPosition = () => {
showScroll.value = document.documentElement.scrollTop > 1500;
};
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
onMounted(() => {
window.addEventListener("scroll", updateScrollPosition);
});
onUnmounted(() => {
window.removeEventListener("scroll", updateScrollPosition);
});
</script>
<template>
<span v-if="showScroll" class="floater overlay-text" @click="scrollToTop">
Scroll To Top
</span>
</template>
<style scoped>
.floater {
display: block;
position: fixed;
right: 10px;
bottom: 10px;
z-index: 1000;
cursor: pointer;
background-color: rgba(112, 128, 144, 0.8);
padding: 0.5rem;
border-radius: 10px;
}
.overlay-text {
color: white;
text-shadow: 1px 1px #6f0b51;
}
</style>

View file

@ -1,32 +0,0 @@
<template>
<div
class="grid grid-rows-2 text-center sm:text-left sm:grid-rows-none sm:grid-cols-2 my-5 navbar w-full"
>
<NuxtLink class="block" to="/">
<h1 class="block site-title bloodseeker">Cinema Corona</h1>
</NuxtLink>
<ul
class="mt-3 sm:mt-0 justify-self-center sm:justify-self-end inline-flex space-x-5 bloodseeker leading-10"
>
<li>
<NuxtLink class="text-xl header-link" to="/lists">Lists</NuxtLink>
</li>
<li>
<NuxtLink class="text-xl header-link" to="/schedule">Schedule</NuxtLink>
</li>
<li v-if="authenticated">
<ProfileMenu />
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { hasToken } from "~/composables/hasToken";
import ProfileMenu from "~/components/common/menus/ProfileMenu.vue";
const authenticated = computed(() => hasToken());
</script>
<style scoped></style>

View file

@ -1,9 +0,0 @@
<script lang="ts" setup></script>
<template>
<button class="btn p-3 mt-5" type="button">
<slot> </slot>
</button>
</template>
<style scoped></style>

View file

@ -1,25 +0,0 @@
<template>
<div v-show="visible" id="movie-modal" class="movie-modal movie-card">
<div class="max-w-4xl mx-auto flex flex-col px-2 sm:px-0">
<span
class="hover-pointer font-bold self-end pr-1 sm:pr-5 pt-5 pb-5 sm:pb-0"
@click="toggleModal()"
>
<span class="bg-red-600 p-2 rounded">X</span>
</span>
<slot></slot>
</div>
</div>
</template>
<script lang="ts" setup>
const visible = ref(false);
const toggleModal = () => {
visible.value = !visible.value;
};
defineExpose({ toggleModal });
</script>
<style scoped></style>

View file

@ -1,21 +0,0 @@
<script lang="ts" setup>
import FormButton from "~/components/common/ui/FormButton.vue";
</script>
<template>
<h3 class="text-bold text-xl">Reset Password</h3>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2">
<label for="old_password">Old Password</label>
<input id="old_password" class="p-3" name="password" type="password" />
</div>
</div>
<div class="flex flex-col gap-2">
<label for="password">New Password</label>
<input id="password" class="p-3" name="password" type="password" />
</div>
<FormButton>Update Password</FormButton>
</template>
<style scoped></style>

View file

@ -27,9 +27,7 @@
</template>
<script lang="ts" setup>
import type { Movie } from "~/types/movie";
const props = defineProps<{ movie: Movie }>();
const props = defineProps(["movie"]);
const emits = defineEmits(["closeModal"]);
const schedule = function (e: Event) {
@ -45,8 +43,7 @@ const schedule = function (e: Event) {
return false;
}
let date = new Date(`${showtime_input}T23:59:00`);
convertToUserTimezone(date);
const date = new Date(`${showtime_input}T00:00:00`);
fetch(`${config.public.apiURL}/showings/`, {
method: "POST",
@ -61,16 +58,12 @@ const schedule = function (e: Event) {
Authorization: `Token ${useCookie("token").value}`,
},
})
.then((_resp) => {
.then((response) => response.json())
.then((_json) => {
emits("closeModal");
})
.catch((err) => alert("Unable to schedule movie. Error:\n" + err));
};
// @todo pull the timezone from a user setting
const convertToUserTimezone = function (date: Date) {
date.toLocaleString("en-US", { timeZone: "America/Chicago" });
};
</script>
<style scoped></style>

View file

@ -1,18 +1,22 @@
<template>
<div v-if="props.movie != null" class="sm:m-5 p-10 movie-card neon-border">
<div>
<h2 id="modal-title" class="row pb-10 text-center sm:text-left">
<h2 id="modal-title" class="row pb-3">
{{ movie.title }} ({{ movie.year }})
</h2>
<div class="grid sm:grid-cols-2 gap-5">
<div class="grid sm:grid-cols-2">
<!-- MODAL POSTER -->
<MoviePoster
:image="movie.poster"
class="max-h-72 max-w-60 sm:max-h-2xl sm:max-w-72 mx-auto sm:mx-0"
/>
<div class="text-end">
<img
id="modal-poster"
:src="movie.poster"
alt="poster"
class="pt-5"
/>
</div>
<div class="mx-auto sm:mx-none">
<div class="pt-5">
<label class="" for="list-picker">Add To List</label><br />
<select id="list-picker" v-model="list_id" class="p-1 text-black">
<option v-for="list in lists" :value="list.id">
@ -34,11 +38,10 @@
<script lang="ts" setup>
import type { MovieList } from "~/types/movielist";
import type { Movie } from "~/types/movie";
const props = defineProps<{ movie: Movie }>();
const props = defineProps(["movie"]);
const list_id = ref(0);
const lists = ref<MovieList[]>(new Array<MovieList>());
const lists = defineModel<MovieList[]>("lists", { default: [] });
const emit = defineEmits<{
(e: "close-modal"): void;
}>();

View file

@ -1,32 +1,23 @@
<template>
<div class="sm:m-5 p-10 movie-card neon-border">
<div>
<LoadingIcon v-if="updating" />
<h2 class="text-xl pb-10 text-center sm:text-left">
<h2 class="text-xl pb-3 text-center sm:text-left">
{{ movie.title }} ({{ movie.year }})
</h2>
<div class="sm:inline-flex sm:space-x-5">
<MoviePoster
:image="movie.poster"
class="max-h-80 max-w-60 mx-auto sm:mx-none"
<img
:src="movie.poster"
alt="movie poster"
class="mx-auto sm:mx-0 neon-border"
/>
<div class="pt-5 sm:pt-0">
<p>{{ movie.plot }}</p>
<ScheduleMovie
v-if="logged_in"
:movie="movie"
class="mt-5"
@close-modal="$emit('close-modal')"
/>
<button
v-if="logged_in"
class="my-10 btn p-2 rounded"
@click="updateMovie"
>
Refresh movie
</button>
</div>
</div>
</div>
@ -35,16 +26,11 @@
<script lang="ts" setup>
import ScheduleMovie from "~/components/forms/ScheduleMovie.vue";
import type { Movie } from "~/types/movie";
defineProps<{ movie: Movie; updating: boolean }>();
const emits = defineEmits(["close-modal", "update-movie"]);
const props = defineProps(["movie"]);
const emits = defineEmits(["close-modal"]);
const logged_in = ref(false);
const updateMovie = function () {
emits("update-movie");
};
onMounted(() => {
const token = useCookie("token").value;
if (token) {

26
src/components/navbar.vue Normal file
View file

@ -0,0 +1,26 @@
<template>
<div class="grid grid-rows-2 text-center sm:text-left sm:grid-rows-none sm:grid-cols-2 my-5 navbar w-full">
<NuxtLink class="block" to="/admin">
<h1 class="block site-title bloodseeker">Cinema Corona</h1>
</NuxtLink>
<ul class="mt-3 sm:mt-0 justify-self-center sm:justify-self-end inline-flex space-x-5 bloodseeker leading-10">
<li>
<NuxtLink class="text-xl header-link" to="/lists">Lists</NuxtLink>
</li>
<li>
<NuxtLink class="text-xl header-link" to="/schedule">Schedule</NuxtLink>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "navbar",
}
</script>
<style scoped>
</style>

View file

@ -1,6 +0,0 @@
import { useCookie } from "#app";
export function hasToken() {
const token = useCookie("token").value;
return token !== null && token !== undefined && token !== "";
}

View file

@ -1,19 +0,0 @@
import { useCookie } from "#app";
export function logout() {
let config = useRuntimeConfig();
fetch(`${config.public.apiURL}/auth/logout/`, {
method: "POST",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
})
.then((response) => response)
.then((_json) => {
let token = useCookie("token");
token.value = null;
navigateTo("/");
})
.catch((err) => console.log(err));
}

View file

@ -1,17 +1,16 @@
<template>
<div class="container mx-auto">
<Navbar />
<NuxtPage class="mb-32" />
<Navbar/>
<NuxtPage/>
</div>
</template>
<script>
import Navbar from "~/components/common/navigation/navbar.vue";
export default {
name: "default",
components: { Navbar },
};
}
</script>
<style scoped></style>
<style scoped>
</style>

View file

@ -9,12 +9,7 @@ export default defineNuxtConfig({
},
},
modules: ["@nuxtjs/tailwindcss", "@vesp/nuxt-fontawesome"],
fontawesome: {
icons: {
solid: ["user"],
},
},
modules: ["@nuxtjs/tailwindcss"],
css: ["@/assets/css/main.css"],
compatibilityDate: "2025-04-05",
@ -29,7 +24,7 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
apiURL: process.env.API_URL || "http://localhost:8000/v1",
apiURL: process.env.API_URL || "http://localhost:8000/api",
},
},

8253
src/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,12 +8,8 @@
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@nuxtjs/tailwindcss": "^6.2.0",
"@types/node": "^22.14.0",
"@vesp/nuxt-fontawesome": "^1.2.1",
"nuxt": "3.x",
"prettier": "3.x",
"typescript": "^5.8.3",
@ -21,8 +17,5 @@
},
"dependencies": {
"lazysizes": "^5.3.2"
},
"overrides": {
"rollup": "4.44.2"
}
}

View file

@ -44,7 +44,7 @@
</div>
<div id="lists" class="hidden">
<lists />
<lists></lists>
</div>
</div>
</template>
@ -54,14 +54,15 @@ import AddMovie from "~/components/modal-content/AddMovie.vue";
import Search from "~/components/admin/search.vue";
import Showings from "~/components/admin/showings.vue";
import Lists from "~/components/admin/lists.vue";
import type { MovieList } from "~/types/movielist";
import { useCookie } from "#app";
import type { Movie } from "~/types/movie";
import Modal from "~/components/common/ui/Modal.vue";
import { logout } from "~/composables/logout";
import Modal from "~/components/Modal.vue";
const modal_movie = ref<Movie | null>(null);
const lists = defineModel<MovieList>("movie-lists", { default: [] });
const modal_movie = defineModel<Movie>("#movie-modal");
const movie_modal = ref<InstanceType<typeof Modal> | null>(null);
const current_view = ref("search");
const closeModal = function () {
movie_modal?.value?.toggleModal();
@ -71,8 +72,7 @@ const showModal = function (movie: Movie) {
movie_modal?.value?.toggleModal();
};
const toggleDisplay = function (element_id: string) {
if (element_id === current_view.value) return;
const tabs = ["search", "showings", "lists"];
let tabs = ["search", "showings", "lists"];
tabs.forEach((value) => {
if (value === element_id) {
@ -80,13 +80,36 @@ const toggleDisplay = function (element_id: string) {
document
.getElementById(element_id + "-tab")
?.classList.toggle("underline");
current_view.value = element_id;
} else if (!document.getElementById(value)?.classList.contains("hidden")) {
document.getElementById(value)?.classList.toggle("hidden");
document.getElementById(value + "-tab")?.classList.toggle("underline");
}
});
};
const logout = () => {
let config = useRuntimeConfig();
fetch(`${config.public.apiURL}/auth/logout/`, {
method: "POST",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
})
.then((response) => response)
.then((_json) => {
let token = useCookie("token");
token.value = null;
navigateTo("/");
})
.catch((err) => console.log(err));
};
onMounted(() => {
const token = useCookie("token").value;
if (!token) {
navigateTo("/");
}
});
</script>
<style scoped></style>

View file

@ -1,81 +1,65 @@
<template>
<div>
<LoadingIcon v-if="loading" show-quote="true" />
<div v-else class="p-5 sm:p-0">
<ScrollToTop></ScrollToTop>
<Modal ref="movie_modal">
<ShowMovie
v-if="modal_movie"
:movie="modal_movie"
:updating="updating"
@close-modal="closeModal"
@update-movie="updateMovie(modal_movie)"
></ShowMovie>
</Modal>
<h2 class="text-xl font-bold pb-5">{{ list?.name }}</h2>
<div
v-if="movies.length > 1 && !loading"
class="grid grid-cols-2 rounded movie-card neon-border p-5"
>
<div>
<ul class="flex flex-row">
<li>
<label class="mr-2" for="hide_scheduled">Hide Scheduled</label>
<input
id="hide_scheduled"
v-model="hide_scheduled"
type="checkbox"
@change="hideScheduled"
/>
</li>
</ul>
</div>
<input
v-model="movie_query"
class="p-1 rounded"
placeholder="Filter Movies"
type="text"
@input="filterMovies"
/>
<div v-if="list_id !== 0" class="p-5 sm:p-0">
<Modal ref="movie_modal">
<ShowMovie
v-if="modal_movie"
:movie="modal_movie"
@close-modal="closeModal"
></ShowMovie>
</Modal>
<h2 class="text-xl font-bold pb-5">{{ list.name }}</h2>
<div class="grid grid-cols-2 rounded movie-card neon-border p-5">
<div>
<ul class="flex flex-row">
<li>
<label class="mr-2" for="hide_scheduled">Hide Scheduled</label>
<input
id="hide_scheduled"
v-model="hide_scheduled"
type="checkbox"
@change="hideScheduled"
/>
</li>
</ul>
</div>
<div
v-if="movies.length < 1 && !loading"
class="mt-10 flex gap-5 flex-col"
>
No Movies Found
<MovieQuote />
</div>
<!-- MOVIE LIST -->
<ul
v-else
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2 mt-5"
>
<li
v-for="movie in filtered_movies"
:key="movie.poster"
class="rounded movie-card neon-border flex flex-col overflow-hidden"
>
<!-- POSTER -->
<MoviePoster
:image="movie.poster"
class="flex-shrink-0"
@click="showModal(movie)"
/>
<div class="p-5 flex flex-col justify-between flex-1">
<!-- TITLE -->
<span class="font-bold text-center mb-1">{{ movie.title }}</span>
<span
v-if="logged_in"
class="text-center hover-pointer"
@click="removeMovie(movie.imdb_id)"
>
X
</span>
</div>
</li>
</ul>
<input
v-model="movie_query"
class="p-1 rounded"
placeholder="Filter Movies"
type="text"
@input="filterMovies"
/>
</div>
<!-- MOVIE LIST -->
<ul
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2 mt-5"
>
<li
v-for="movie in filtered_movies"
:key="movie.id"
class="rounded movie-card neon-border"
>
<!-- POSTER -->
<img
:data-src="movie.poster"
alt="movie poster"
class="lazyload p-3 movie-poster hover-pointer mx-auto"
@click="showModal(movie)"
/>
<div class="p-5 flex flex-col">
<!-- TITLE -->
<span class="font-bold text-center mb-1">{{ movie.title }}</span>
<span
v-if="logged_in"
class="text-center hover-pointer"
@click="removeMovie(movie.id)"
>
X
</span>
</div>
</li>
</ul>
</div>
</template>
@ -84,58 +68,56 @@ import ShowMovie from "~/components/modal-content/ShowMovie.vue";
import "lazysizes";
import type { MovieList } from "~/types/movielist";
import type { Movie } from "~/types/movie";
import Modal from "~/components/common/ui/Modal.vue";
import { useCookie } from "#app";
import { $fetch } from "ofetch";
import MoviePoster from "~/components/MoviePoster.vue";
import ScrollToTop from "~/components/common/navigation/ScrollToTop.vue";
import Modal from "~/components/Modal.vue";
const list_id = ref(0);
const list = ref<MovieList | null>(null);
const loading = ref(true);
const list = defineModel<MovieList>("movie_list", { default: [] });
const modal_movie: Ref<Movie | null> = ref(null);
const movies = ref<Movie[]>(new Array<Movie>());
const filtered_movies = ref<Movie[]>(new Array<Movie>());
const movies = defineModel<Movie[] | []>("movies", {
default: [],
});
const filtered_movies = defineModel<Movie[]>("filtered_movies");
const movie_query = ref("");
const logged_in = ref(false);
const hide_scheduled = ref(false);
const getList = async function (list_id: number) {
loading.value = true;
let config = useRuntimeConfig();
let headers: any = {
"Content-type": "application/json",
};
const { data, error } = await useFetch<MovieList>(
`${config.public.apiURL}/lists/${list_id}`,
{
method: "GET",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
},
);
if (typeof useCookie("token").value !== "undefined") {
headers["Authorization"] = `Token ${useCookie("token").value}`;
}
$fetch<MovieList>(`${config.public.apiURL}/lists/${list_id}`, {
method: "GET",
headers: headers,
})
.then((data) => {
list.value = data;
movies.value = data?.movies || new Array<Movie>();
if (error.value) {
if (error.value.statusCode === 401) {
navigateTo("/");
}
if (error.value.statusCode === 404) {
alert("List not found");
navigateTo("/lists");
}
} else {
if (!data.value) {
alert("List not found");
navigateTo("/lists");
} else {
list.value = data.value;
movies.value = data.value?.movies || [];
filtered_movies.value = movies.value;
loading.value = false;
})
.catch((err) => {
if (err.statusCode === 401) {
navigateTo("/");
}
if (err.statusCode === 404) {
alert("List not found");
navigateTo("/lists");
}
});
}
}
};
const hideScheduled = function () {
if (hide_scheduled.value && movies.value.length > 0) {
if (hide_scheduled && movies.value.length > 0) {
let filtered = movies.value.filter((movie) => {
return !movie.has_been_scheduled;
return movie.last_watched === null;
});
if (typeof filtered != "undefined") {
filtered_movies.value = filtered;
@ -145,7 +127,7 @@ const hideScheduled = function () {
}
};
const removeMovie = async function (movie_id: string) {
const removeMovie = async function (movie_id: number) {
let config = useRuntimeConfig();
let confirmed = confirm("Remove movie from list?");
@ -184,38 +166,6 @@ const removeMovie = async function (movie_id: string) {
}
};
const updating = ref(false);
const updateMovie = async function (movie: Movie) {
let config = useRuntimeConfig();
updating.value = true;
$fetch<Movie>(`${config.public.apiURL}/movies/${movie.id}/`, {
method: "PUT",
headers: {
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
body: JSON.stringify(movie),
})
.then((data) => {
modal_movie.value = data || [];
movies.value = movies.value.map((movie) => {
return movie.id === data.id ? data : movie;
});
filtered_movies.value = movies.value;
updating.value = false;
})
.catch((err) => {
if (err.statusCode === 401) {
navigateTo("/");
}
if (err.statusCode === 404) {
alert("Unable to update movie");
}
updating.value = false;
});
};
const filterMovies = function () {
if (!movie_query) {
filtered_movies.value = movies.value;
@ -243,7 +193,6 @@ onMounted(() => {
const route = useRoute();
if (typeof route.params.id === "string") {
const list_param: string = route.params.id;
list_id.value = parseInt(list_param);
getList(list_id.value);
}

View file

@ -1,16 +1,8 @@
<template>
<div>
<LoadingIcon
v-if="loading"
class="w-full p-5 sm:p-0 max-w-2xl"
show-quote="true"
/>
<div v-if="lists.length < 1 && !loading" class="flex flex-col gap-10">
<div class="p-5 sm:p-0">
<div v-if="lists.length < 1">
<p>No lists found</p>
<MovieQuote />
</div>
<ul class="grid grid-cols-2 gap-3 mt-5">
<li v-for="list in lists" class="movie-card neon-border p-5 rounded">
<div class="grid grid-rows-2 gap-3">
@ -28,11 +20,8 @@
import type { MovieList } from "~/types/movielist";
import { useCookie } from "#app";
const lists = ref<MovieList[]>(new Array<MovieList>());
const loading = ref(true);
const lists = defineModel<MovieList[]>("movie_list", { default: [] });
const updateLists = async function () {
loading.value = true;
let config = useRuntimeConfig();
let headers: any = {
"Content-type": "application/json",
@ -42,20 +31,21 @@ const updateLists = async function () {
headers["Authorization"] = `Token ${useCookie("token").value}`;
}
await $fetch<MovieList[]>(`${config.public.apiURL}/lists`, {
method: "GET",
headers: headers,
})
.then((data) => {
lists.value = data || new Array<MovieList>();
loading.value = false;
})
.catch((err) => {
if (err.statusCode === 401) {
useCookie("token").value = null;
navigateTo("/");
}
});
const { data, error } = await useFetch<MovieList[]>(
`${config.public.apiURL}/lists`,
{
method: "GET",
headers: headers,
},
);
if (error.value) {
if (error.value.statusCode === 401) {
navigateTo("/");
}
} else {
lists.value = data.value || [];
}
};
onMounted(() => {

View file

@ -1,19 +1,44 @@
<template>
<div class="p-5 sm:p-0">
<LoadingIcon v-if="loading" show-quote="true" />
<div v-else>
<div
v-if="schedule && schedule?.showings.length < 1 && !loading"
class="p-5"
<div v-if="schedule && schedule?.showings.length < 1" class="p-5">
<span>No Showings Found</span>
</div>
<ul class="flex flex-col gap-5">
<li
v-for="showing in schedule?.showings"
class="p-5 movie-card neon-border"
>
<span>No Showings Found</span>
<MovieQuote />
</div>
<div class="sm:grid grid-cols-2 lg:grid-cols-3">
<img
:src="showing.movie.poster"
alt="Movie Poster"
class="mx-auto mb-5 sm:mb-0 sm:mx-0 neon-border bg-black schedule-poster"
/>
<div class="self-center text-left">
<h5 class="text-center sm:text-left mb-3 text-xl">
{{ showing.movie.title }}
</h5>
<h5 class="text-center sm:text-left mb-3">
{{ formatDate(showing.showtime) }}
</h5>
<span class="">{{ showing.movie.plot }}</span>
</div>
</div>
</li>
</ul>
<!-- PREVIOUS SHOWINGS -->
<div id="previous-showings" class="mt-5 list-group">
<span
class="block mb-5 hover-pointer underline"
@click="getSchedule(true)"
>
Previous Showings
</span>
<span id="loader" class="hidden">Loading...</span>
<ul class="flex flex-col gap-5">
<li
v-for="showing in schedule?.showings"
class="p-5 movie-card neon-border"
>
<li v-for="showing in past_showings" class="p-5 movie-card neon-border">
<div class="sm:grid grid-cols-2 lg:grid-cols-3">
<img
:src="showing.movie.poster"
@ -22,48 +47,13 @@
/>
<div class="self-center text-left">
<h5 class="text-center sm:text-left mb-3 text-xl">
{{ showing.movie.title }}
</h5>
<h5 class="text-center sm:text-left mb-3">
{{ formatDate(showing.showtime) }}
</h5>
<h5 class="text-xl mb-3">{{ showing.movie.title }}</h5>
<h5 class="mb-3">{{ formatDate(showing.showtime) }}</h5>
<span class="">{{ showing.movie.plot }}</span>
</div>
</div>
</li>
</ul>
<!-- PREVIOUS SHOWINGS -->
<LoadingIcon v-if="loadingPrevious" />
<div v-else id="previous-showings" class="p-5 mt-5 list-group">
<span
class="block mb-5 hover-pointer underline"
@click="getSchedule(true)"
>
Previous Showings
</span>
<ul class="flex flex-col gap-5">
<li
v-for="showing in past_showings"
class="p-5 movie-card neon-border"
>
<div class="sm:grid grid-cols-2 lg:grid-cols-3">
<img
:src="showing.movie.poster"
alt="Movie Poster"
class="mx-auto mb-5 sm:mb-0 sm:mx-0 neon-border bg-black schedule-poster"
/>
<div class="self-center text-left">
<h5 class="text-xl mb-3">{{ showing.movie.title }}</h5>
<h5 class="mb-3">{{ formatDate(showing.showtime) }}</h5>
<span class="">{{ showing.movie.plot }}</span>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
@ -71,72 +61,78 @@
<script lang="ts" setup>
import type { Showing } from "~/types/showing";
import type { Schedule } from "~/types/schedule";
import { $fetch } from "ofetch";
import { useCookie } from "#app";
const schedule = ref<Schedule>();
const past_showings = ref<Showing[]>(new Array<Showing>());
const loading = ref(true);
const loadingPrevious = ref(false);
const schedule = defineModel<Schedule>("schedule");
const past_showings = defineModel<Showing[]>("past_showings", {
default: [],
});
const got_previous = ref(false);
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const formatDate = function (date_string: string) {
let date = new Date(date_string);
//let month = months[date.getMonth()];
let month = months[date.getMonth()];
//return `${month} ${date.getDate()}, ${date.getFullYear()}`;
return new Intl.DateTimeFormat("en-US", {
month: "long",
day: "numeric",
year: "numeric",
}).format(date);
return `${month} ${date.getDate()}, ${date.getFullYear()}`;
};
const getSchedule = async function (previous = false) {
if (previous) loadingPrevious.value = true;
else loading.value = true;
let config = useRuntimeConfig();
if (got_previous.value) {
return false;
}
document.getElementById("loader")?.classList.toggle("hidden");
let params = "";
if (previous) params = "?past_showings=true";
let headers: any = {
"Content-type": "application/json",
};
const { data, error } = await useFetch<Schedule>(
`${config.public.apiURL}/schedules/1${params}`,
{
method: "GET",
headers: {
Accept: "application/json",
"Content-type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
},
);
if (typeof useCookie("token").value !== "undefined") {
headers["Authorization"] = `Token ${useCookie("token").value}`;
}
await $fetch(`${config.public.apiURL}/schedules/1${params}`, {
method: "GET",
headers: headers,
})
.then((data) => {
if (error.value) {
if (error.value.statusCode === 401) {
navigateTo("/");
}
if (error.value.statusCode === 404) {
alert("Schedule not found");
navigateTo("/");
}
} else {
if (!data.value) {
alert("Schedule not found");
navigateTo("/");
} else {
if (previous) {
past_showings.value = data.past_showings;
past_showings.value = data.value.past_showings;
} else {
schedule.value = data;
schedule.value = data.value;
}
loading.value = false;
loadingPrevious.value = false;
})
.catch((err) => {
switch (err.statusCode) {
case 401:
useCookie("token").value = null;
navigateTo("/");
break;
case 404:
alert("Unable to find schedule");
navigateTo("/");
break;
}
});
document.getElementById("loader")?.classList.toggle("hidden");
}
}
return schedule;
};

View file

@ -1,143 +0,0 @@
<script lang="ts" setup>
import { useCookie } from "#app";
import type { UserProfile } from "~/types/userProfile";
const config = useRuntimeConfig();
const profile = ref<UserProfile | null>(null);
const loading = ref(true);
const getProfile = async function () {
await $fetch<UserProfile>(`${config.public.apiURL}/users/profile`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${useCookie("token").value}`,
},
})
.then((data) => {
profile.value = data;
loading.value = false;
})
.catch((err) => {
if (err.statusCode === 401) {
useCookie("token").value = null;
navigateTo("/");
}
});
};
const formatDate = function (date_string: string) {
return new Date(date_string).toLocaleDateString();
};
onMounted(getProfile);
</script>
<template>
<LoadingIcon v-if="loading" show-quote="true" />
<div v-else>
<h2 class="page-header">Profile</h2>
<div id="profile-card" class="movie-card neon-border">
<div id="user-data">
<div id="profile-picture">
<img
alt="profile image"
class="user-icon neon-border"
src="https://placecage.lucidinternets.com/g/200/200"
/>
</div>
<ul class="profile-details">
<li class="user-detail">
<label for="name">Name</label>
<span id="name">{{ profile?.name || profile?.username }}</span>
</li>
<li class="user-detail">
<label for="username">Username</label>
<span id="username">{{ profile?.username }}@movienight.social</span>
</li>
<li class="user-detail">
<label for="date-joined">Date Joined</label>
<span id="date-joined">{{
formatDate(profile?.date_joined || "")
}}</span>
</li>
</ul>
</div>
<hr class="neon-border my-5" />
<div id="extra-fields">
<div id="movielists">
<h3 class="section-header">Lists</h3>
<ul id="movielist-list">
<li v-for="list in profile?.lists">
<NuxtLink :to="`/lists/${list.id}`">{{ list.name }}</NuxtLink>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<style scoped>
label {
font-weight: bold;
font-size: 1.2rem;
}
.section-header {
font-size: 1.5rem;
line-height: calc(2 / 1.5);
padding-bottom: 1rem;
}
#user-data {
display: flex;
flex-direction: column;
align-items: center;
}
.user-detail {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.user-icon {
object-fit: cover;
border-radius: 2rem;
}
.profile-details {
display: flex;
flex-direction: column;
gap: 1rem;
list-style: none;
margin: 3rem 0;
}
.movie-card {
padding: 2rem;
}
.movie-review {
display: flex;
flex-direction: row;
gap: 1rem;
}
#extra-fields {
display: flex;
flex-direction: column;
gap: 2rem;
}
@media (width >= 48rem) {
#user-data {
flex-direction: row;
gap: 5rem;
}
}
</style>

View file

@ -1,50 +0,0 @@
<script lang="ts" setup>
import PasswordResetForm from "~/components/forms/PasswordReset.vue";
const timezones = Intl.supportedValuesOf("timeZone");
</script>
<template>
<div>
<h2 class="page-header">Settings</h2>
<div class="movie-card neon-border">
<form action="#" class="flex flex-col gap-5">
<label class="text-bold text-xl" for="site-name">Site Name</label>
<input
id="site-name"
class="p-3"
name="site-name"
placeholder="Movie Night"
type="text"
/>
<h3 class="text-bold text-xl">Locale</h3>
<!--SET TIMEZONE -->
<div class="flex flex-col gap-2">
<label for="timezone">Timezone</label>
<select id="timezone" name="timezone">
<option v-for="timezone in timezones" :value="timezone">
{{ timezone }}
</option>
</select>
</div>
<hr class="my-5 neon-border" />
<PasswordResetForm />
</form>
</div>
</div>
</template>
<style scoped>
.movie-card {
padding: 2em;
}
#timezone {
color: black;
padding: 1em;
}
</style>

View file

@ -10,5 +10,5 @@ export type Movie = {
actors: string;
plot: string;
poster: string;
has_been_scheduled: boolean;
last_watched: string;
};

View file

@ -1,8 +0,0 @@
import type { MovieList } from "~/types/movielist";
export type UserProfile = {
name: string;
username: string;
date_joined: string;
lists: MovieList[];
};