Reinitialized commit. Update and actors overview with some filters.
This commit is contained in:
30
components/actors/actors.vue
Normal file
30
components/actors/actors.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<ul class="actors nolist">
|
||||
<li
|
||||
v-for="actor in actors"
|
||||
:key="`actor-${actor.id}`"
|
||||
class="actor"
|
||||
>
|
||||
<ActorTile :actor="actor" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ActorTile from './tile.vue';
|
||||
|
||||
defineProps({
|
||||
actors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 10rem);
|
||||
gap: .5rem;
|
||||
}
|
||||
</style>
|
||||
39
components/actors/gender.vue
Executable file
39
components/actors/gender.vue
Executable file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="gender"
|
||||
class="gender"
|
||||
:class="{ [gender]: true }"
|
||||
>
|
||||
<Icon :icon="gender" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
gender: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gender {
|
||||
&.female .icon {
|
||||
fill: var(--female);
|
||||
filter: drop-shadow(0 0 1px var(--shadow-weak-20));
|
||||
}
|
||||
|
||||
&.male .icon {
|
||||
fill: var(--male);
|
||||
filter: drop-shadow(0 0 1px var(--shadow-weak-20));
|
||||
}
|
||||
|
||||
&.transsexual .icon {
|
||||
fill: var(--text-light);
|
||||
filter: drop-shadow(1px 0 0 var(--female)) drop-shadow(-1px 0 0 var(--female)) drop-shadow(0 1px 0 var(--female)) drop-shadow(0 -1px 0 var(--female))
|
||||
drop-shadow(1px 0 0 var(--male)) drop-shadow(-1px 0 0 var(--male)) drop-shadow(0 1px 0 var(--male)) drop-shadow(0 -1px 0 var(--male))
|
||||
drop-shadow(0 0 1px var(--shadow-weak-20))
|
||||
}
|
||||
}
|
||||
</style>
|
||||
147
components/actors/tile.vue
Normal file
147
components/actors/tile.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="actor">
|
||||
<span class="name">{{ actor.name }}</span>
|
||||
|
||||
<Link
|
||||
:href="`/actor/${actor.id}/${actor.slug}`"
|
||||
class="avatar-link no-link"
|
||||
>
|
||||
<img
|
||||
v-if="actor.avatar"
|
||||
:src="actor.avatar.isS3 ? `https://cdndev.traxxx.me/${actor.avatar.thumbnail}` : `/media/${actor.avatar.thumbnail}`"
|
||||
:style="{ 'background-image': actor.avatar.isS3 ? `url(https://cdndev.traxxx.me/${actor.avatar.lazy})` : `url(/media/${actor.avatar.lazy})` }"
|
||||
class="avatar"
|
||||
>
|
||||
</Link>
|
||||
|
||||
<div class="details">
|
||||
<span class="birth">
|
||||
<Gender :gender="actor.gender" />
|
||||
|
||||
<span
|
||||
v-if="actor.ageFromBirth"
|
||||
:title="`Born ${formatDate(actor.dateOfBirth, 'MMMM d, yyyy')}`"
|
||||
class="age"
|
||||
>{{ actor.ageFromBirth }}</span>
|
||||
|
||||
<span
|
||||
v-if="actor.ageThen && actor.ageThen < actor.ageFromBirth"
|
||||
class="age age-then"
|
||||
>{{ actor.ageThen }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="actor.birthCountry"
|
||||
:title="`Born in ${actor.birthCountry.name}`"
|
||||
class="country"
|
||||
>
|
||||
{{ actor.birthCountry.alpha2 }}
|
||||
<img
|
||||
:src="`/flags/${actor.birthCountry.alpha2.toLowerCase()}.svg`"
|
||||
class="flag"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatDate } from '#/src/format.js';
|
||||
|
||||
import Gender from './gender.vue';
|
||||
|
||||
defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 10rem;
|
||||
height: 15rem;
|
||||
position: relative;
|
||||
border-radius: .25rem;
|
||||
margin: .25rem;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
||||
|
||||
.name {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
padding: .25rem .5rem;
|
||||
font-weight: bold;
|
||||
font-size: .9rem;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.avatar-link {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center 0;
|
||||
background-size: cover;
|
||||
background-position: center 0;
|
||||
}
|
||||
|
||||
.details {
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0 .5rem;
|
||||
color: var(--text-light);
|
||||
background: var(--shadow);
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gender {
|
||||
display: inline-block;
|
||||
padding: .25rem 0;
|
||||
margin-right: .25rem;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.birth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.age {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
.age-then {
|
||||
color: var(--shadow-weak-10);
|
||||
}
|
||||
|
||||
.country {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flag {
|
||||
height: .75rem;
|
||||
margin-left: .25rem;
|
||||
}
|
||||
</style>
|
||||
208
components/filters/range.vue
Executable file
208
components/filters/range.vue
Executable file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div class="filter-section">
|
||||
<label class="filter-label noselect">
|
||||
<span class="label">
|
||||
<Checkbox
|
||||
:checked="!disabled"
|
||||
class="checkbox"
|
||||
@change="(checked) => $emit('enable', checked)"
|
||||
/>{{ label }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="!disabled"
|
||||
class="label-values"
|
||||
>{{ value[0] }} - {{ value[1] }}<template v-if="unit"> {{ unit }}</template></span>
|
||||
</label>
|
||||
|
||||
<span class="filter-split">
|
||||
<Range
|
||||
:min="min"
|
||||
:max="max"
|
||||
:value="value"
|
||||
:values="values"
|
||||
:disabled="disabled"
|
||||
:allow-enable="allowEnable"
|
||||
@enable="emit('enable', true)"
|
||||
@input="(range) => emit('input', range)"
|
||||
@change="(range) => emit('change', range)"
|
||||
>
|
||||
<template #start><slot name="start" /></template>
|
||||
<template #end><slot name="end" /></template>
|
||||
</Range>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Checkbox from '../form/checkbox.vue';
|
||||
import Range from '../form/range.vue';
|
||||
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [0, 10],
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowEnable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change', 'input', 'enable']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-section {
|
||||
width: 15rem;
|
||||
max-width: 100%;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: .75rem .5rem .5rem .5rem;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
font-size: .9rem;
|
||||
|
||||
.label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin: 0 .75rem 0 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label-values {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.filter-split {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggle-container,
|
||||
.range-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
|
||||
&.on {
|
||||
.toggle-label.on {
|
||||
color: var(--enabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--enabled);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
background-color: var(--enabled-background);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
background: var(--enabled);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
background: var(--enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.off {
|
||||
.toggle-label.off {
|
||||
color: var(--disabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
background-color: var(--disabled-background);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
background: var(--disabled);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
background: var(--disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
min-width: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
padding: 0 .5rem;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
font-size: .9rem;
|
||||
|
||||
&.on {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
&.on {
|
||||
color: var(--enabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--enabled);
|
||||
}
|
||||
}
|
||||
|
||||
&.off {
|
||||
color: var(--disabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
102
components/form/checkbox.vue
Executable file
102
components/form/checkbox.vue
Executable file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<label class="check-container noselect">
|
||||
<span
|
||||
v-if="label"
|
||||
class="check-label"
|
||||
>{{ label }}</span>
|
||||
|
||||
<input
|
||||
v-show="false"
|
||||
:id="`checkbox-${uid}`"
|
||||
:checked="checked"
|
||||
type="checkbox"
|
||||
class="check-checkbox"
|
||||
@change="$emit('change', $event.target.checked)"
|
||||
>
|
||||
|
||||
<label
|
||||
:for="`checkbox-${uid}`"
|
||||
class="check"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const uid = String(Math.random()).slice(2);
|
||||
|
||||
defineProps({
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['change']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.check-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.check {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
background-color: var(--shadow-weak-30);
|
||||
border-radius: .25rem;
|
||||
cursor: pointer;
|
||||
transition: background .15s ease;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: .5rem;
|
||||
height: .3rem;
|
||||
border: solid 2px var(--text-light);
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
margin: -.2rem 0 0 0;
|
||||
transform: rotateZ(-45deg) scaleX(0);
|
||||
transition: transform .15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.check-cross .check::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: url('/img/icons/cross3.svg') no-repeat center/80%;
|
||||
opacity: .15;
|
||||
transition: transform .1s ease;
|
||||
}
|
||||
|
||||
.check-checkbox:checked + .check {
|
||||
background: var(--primary);
|
||||
|
||||
&::after {
|
||||
transform: rotateZ(-45deg) scaleX(1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.check-label {
|
||||
overflow: hidden;
|
||||
text-transform: capitalize;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
</style>
|
||||
225
components/form/range.vue
Executable file
225
components/form/range.vue
Executable file
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div class="range-container">
|
||||
<div
|
||||
class="label label-start"
|
||||
:class="{ disabled }"
|
||||
@click="setRange(range.a === minValue ? 'a' : 'b', min)"
|
||||
>
|
||||
<slot name="start" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="range"
|
||||
:class="{ disabled }"
|
||||
:style="{ background: `linear-gradient(90deg, var(--slider-track) ${minPercentage}%, var(--slider-range) ${minPercentage}%, var(--slider-range) ${maxPercentage}%, var(--slider-track) ${maxPercentage}%)` }"
|
||||
@click="setNearest"
|
||||
>
|
||||
<input
|
||||
v-model.number="range.a"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:data-value="range.a"
|
||||
:disabled="disabled"
|
||||
type="range"
|
||||
class="slider"
|
||||
@input="set('input')"
|
||||
@change="set('change')"
|
||||
@click.stop
|
||||
>
|
||||
|
||||
<input
|
||||
v-model.number="range.b"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:data-value="range.b"
|
||||
:disabled="disabled"
|
||||
type="range"
|
||||
class="slider"
|
||||
@input="set('input')"
|
||||
@change="set('change')"
|
||||
@click.stop
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="label label-end"
|
||||
:class="{ disabled }"
|
||||
@click="setRange(range.b === maxValue ? 'b' : 'a', max)"
|
||||
>
|
||||
<slot name="end" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
nextTick,
|
||||
} from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [3, 7],
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowEnable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change', 'input', 'enable']);
|
||||
|
||||
const range = ref({
|
||||
a: props.values ? props.values.indexOf(props.value[0]) : props.value[0],
|
||||
b: props.values ? props.values.indexOf(props.value[1]) : props.value[1],
|
||||
});
|
||||
|
||||
const minValue = computed(() => Math.min(range.value.a, range.value.b));
|
||||
const maxValue = computed(() => Math.max(range.value.a, range.value.b));
|
||||
const minPercentage = computed(() => ((minValue.value - props.min) / (props.max - props.min)) * 100);
|
||||
const maxPercentage = computed(() => ((maxValue.value - props.min) / (props.max - props.min)) * 100);
|
||||
|
||||
function set(type = 'change') {
|
||||
if (props.values) {
|
||||
emit(type, [props.values[minValue.value], props.values[maxValue.value]]);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(type, [minValue.value, maxValue.value]);
|
||||
}
|
||||
|
||||
async function setNearest(event) {
|
||||
if (props.allowEnable) {
|
||||
emit('enable');
|
||||
}
|
||||
|
||||
await nextTick;
|
||||
|
||||
if (!props.disabled) {
|
||||
const closestValue = Math.round((event.offsetX / event.target.getBoundingClientRect().width) * (props.max - props.min)) + props.min;
|
||||
const closestSlider = Math.abs(range.value.a - closestValue) < Math.abs(range.value.b - closestValue) ? 'a' : 'b';
|
||||
|
||||
range.value[closestSlider] = closestValue;
|
||||
|
||||
emit('change', [minValue.value, maxValue.value]);
|
||||
}
|
||||
}
|
||||
|
||||
async function setRange(prop, value) {
|
||||
if (props.allowEnable) {
|
||||
emit('enable');
|
||||
}
|
||||
|
||||
await nextTick;
|
||||
|
||||
if (!props.disabled) {
|
||||
range.value[prop] = value;
|
||||
|
||||
emit('change', [minValue.value, maxValue.value]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dark .range-container .range {
|
||||
--slider-range: var(--lighten-weak-10);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.range-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.range {
|
||||
--slider-track: var(--shadow-weak-30);
|
||||
--slider-range: var(--primary-faded);
|
||||
--slider-thumb: var(--primary);
|
||||
|
||||
position: relative;
|
||||
height: 1.25rem;
|
||||
flex-grow: 1;
|
||||
border-radius: .625rem;
|
||||
|
||||
&.disabled {
|
||||
--slider-range: var(--shadow-weak-10);
|
||||
--slider-thumb: var(--grey-dark-10);
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
background: none;
|
||||
outline: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
display: block;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--slider-thumb);
|
||||
pointer-events: visible;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
appearance: none;
|
||||
display: block;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--slider-thumb);
|
||||
pointer-events: visible;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 .5rem;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
cursor: pointer;
|
||||
|
||||
::v-deep(.icon) {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.icon) {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
fill: var(--shadow);
|
||||
}
|
||||
</style>
|
||||
115
components/form/toggle.vue
Executable file
115
components/form/toggle.vue
Executable file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<label
|
||||
class="toggle-container noselect"
|
||||
:class="{ light: $store.state.ui.theme === 'dark' }"
|
||||
>
|
||||
<input
|
||||
:id="`toggle-${id}`"
|
||||
:checked="checked"
|
||||
:true-value="trueValue"
|
||||
:false-value="falseValue"
|
||||
:disabled="disabled"
|
||||
type="checkbox"
|
||||
class="toggle-input"
|
||||
@change="$emit('change', $event.target.checked)"
|
||||
>
|
||||
|
||||
<label
|
||||
:for="`toggle-${id}`"
|
||||
class="toggle"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
trueValue: {
|
||||
type: null,
|
||||
default: true,
|
||||
},
|
||||
falseValue: {
|
||||
type: null,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
id: Math.floor(new Date().getTime() * Math.random()),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.toggle-container {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
&.light {
|
||||
.toggle {
|
||||
background: var(--lighten-weak);
|
||||
}
|
||||
|
||||
.toggle-input:checked + .toggle {
|
||||
background: var(--lighten);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
width: 2rem;
|
||||
height: .9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background: var(--shadow-hint);
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: var(--background-light);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 2px var(--darken-strong);
|
||||
transition: background-color .2s ease, left .2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-input {
|
||||
display: none;
|
||||
|
||||
&:checked + .toggle {
|
||||
background: var(--primary-faded);
|
||||
|
||||
&::after {
|
||||
background: var(--primary);
|
||||
left: calc(100% - 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] + .toggle {
|
||||
background: var(--shadow-weak);
|
||||
|
||||
&::after {
|
||||
background: var(--shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100
components/header/header.vue
Normal file
100
components/header/header.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<Link href="/">
|
||||
<h1 class="title">
|
||||
<div
|
||||
class="logo"
|
||||
v-html="logo"
|
||||
/>
|
||||
</h1>
|
||||
</Link>
|
||||
|
||||
<nav class="nav">
|
||||
<ul class="nav-list nolist">
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="link"
|
||||
href="/updates"
|
||||
>Updates</Link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="link"
|
||||
href="/actors"
|
||||
>Actors</Link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="link"
|
||||
href="/channels"
|
||||
>Channels</Link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="link"
|
||||
href="/tags"
|
||||
>Tags</Link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="link"
|
||||
href="/movies"
|
||||
>Movies</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import logo from '../../assets/img/logo.svg?raw'; // eslint-disable-line import/no-unresolved
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
width: 8rem;
|
||||
height: 3rem;
|
||||
padding: .75rem;
|
||||
margin-right: 1rem;
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nav-item .link {
|
||||
font-size: .9rem;
|
||||
color: var(--shadow-strong-10);
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
font-weight: bold;
|
||||
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
components/icon/icon.vue
Executable file
58
components/icon/icon.vue
Executable file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ active }"
|
||||
:title="title"
|
||||
class="icon"
|
||||
v-html="svg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const svg = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
import(`../../assets/img/icons/${props.icon}.svg?raw`).then((raw) => {
|
||||
svg.value = raw.default;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
fill: var(--text);
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
font-size: 0; /* prevent flashing null before icon is loaded */
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.active {
|
||||
fill: var(--shadow);
|
||||
|
||||
&:hover {
|
||||
fill: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
components/link/link.vue
Normal file
21
components/link/link.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<a
|
||||
:class="{ active: pageContext.urlPathname === $attrs.href || active }"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
</script>
|
||||
175
components/pagination/pagination.vue
Normal file
175
components/pagination/pagination.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<nav class="pagination">
|
||||
<ul class="pages nolist">
|
||||
<li>
|
||||
<Link
|
||||
:href="`/updates/${routeParams?.scope}/1`"
|
||||
:class="{ disabled: !hasPrevPage }"
|
||||
class="page first nolink"
|
||||
><Icon icon="first2" /></Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link
|
||||
:href="hasPrevPage ? `/updates/${routeParams?.scope}/${currentPage - 1}` : null"
|
||||
:class="{ disabled: !hasPrevPage }"
|
||||
class="page prev nolink"
|
||||
><Icon icon="arrow-left" /></Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="index">
|
||||
<ul class="pages before wrap nolist">
|
||||
<li
|
||||
v-for="page in prevPages"
|
||||
:key="`page-${page}`"
|
||||
>
|
||||
<Link
|
||||
:href="`/updates/${routeParams?.scope}/${page}`"
|
||||
class="page nolink"
|
||||
:class="{ active: page === currentPage }"
|
||||
>{{ page }}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="pages nolist">
|
||||
<li>
|
||||
<div class="page active">{{ currentPage }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="pages after wrap nolist">
|
||||
<li
|
||||
v-for="page in nextPages"
|
||||
:key="`page-${page}`"
|
||||
>
|
||||
<Link
|
||||
:href="`/updates/${routeParams?.scope}/${page}`"
|
||||
class="page nolink"
|
||||
:class="{ active: page === currentPage }"
|
||||
>{{ page }}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="pages nolist">
|
||||
<li>
|
||||
<Link
|
||||
:href="hasNextPage ? `/updates/${routeParams?.scope}/${currentPage + 1}` : null"
|
||||
:class="{ disabled: !hasNextPage }"
|
||||
class="page next nolink"
|
||||
><Icon icon="arrow-right" /></Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link
|
||||
:href="`/updates/${routeParams?.scope}/${pageTotal}`"
|
||||
:class="{ disabled: !hasNextPage }"
|
||||
class="page last nolink"
|
||||
><Icon icon="last2" /></Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
const { routeParams, pageProps } = inject('pageContext');
|
||||
const currentPage = Number(routeParams?.page);
|
||||
const limit = Number(pageProps.limit) || 30;
|
||||
const total = Number(pageProps.total);
|
||||
const pageTotal = Math.ceil(total / limit);
|
||||
const hasNextPage = currentPage + 1 <= pageTotal;
|
||||
const hasPrevPage = currentPage - 1 >= 1;
|
||||
|
||||
console.log(routeParams);
|
||||
|
||||
const prevPages = Array.from({ length: 4 }, (value, index) => {
|
||||
const page = currentPage - index - 1;
|
||||
|
||||
if (page < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return page;
|
||||
}).filter(Boolean);
|
||||
|
||||
const nextPages = Array.from({ length: 4 }, (value, index) => {
|
||||
const page = currentPage + index + 1;
|
||||
|
||||
if (page > pageTotal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return page;
|
||||
}).filter(Boolean);
|
||||
|
||||
console.log(total, limit, currentPage);
|
||||
console.log(prevPages);
|
||||
console.log(nextPages);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination {
|
||||
height: 5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
font-size: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pages {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.before {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.index {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
|
||||
.icon {
|
||||
width: .9rem;
|
||||
height: .9rem;
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.disabled .icon {
|
||||
fill: var(--shadow-weak-20);
|
||||
}
|
||||
}
|
||||
|
||||
.prev {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.next {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
</style>
|
||||
81
components/scenes/scenes.vue
Normal file
81
components/scenes/scenes.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<nav class="scopes">
|
||||
<Link
|
||||
href="/updates/latest"
|
||||
class="scope nolink"
|
||||
:active="scope === 'latest'"
|
||||
>Latest</Link>
|
||||
|
||||
<Link
|
||||
href="/updates/upcoming"
|
||||
class="scope nolink"
|
||||
:active="scope === 'upcoming'"
|
||||
>Upcoming</Link>
|
||||
|
||||
<Link
|
||||
href="/updates/new"
|
||||
class="scope nolink"
|
||||
:active="scope === 'new'"
|
||||
>New</Link>
|
||||
</nav>
|
||||
|
||||
<ul class="scenes nolist">
|
||||
<li
|
||||
v-for="scene in scenes"
|
||||
:key="scene.id"
|
||||
>
|
||||
<Scene :scene="scene" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Pagination />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, inject } from 'vue';
|
||||
|
||||
import Scene from './tile.vue';
|
||||
import Pagination from '../pagination/pagination.vue';
|
||||
|
||||
const { routeParams } = inject('pageContext');
|
||||
const { scope } = routeParams;
|
||||
|
||||
defineProps({
|
||||
scenes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
background: var(--background-base-10);
|
||||
}
|
||||
|
||||
.scenes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
|
||||
gap: .75rem .5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.scopes {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.scope {
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
color: var(--shadow);
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
188
components/scenes/tile.vue
Normal file
188
components/scenes/tile.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="tile">
|
||||
<Link
|
||||
:href="`/scene/${scene.id}/${scene.slug}`"
|
||||
target="_blank"
|
||||
class="poster"
|
||||
>
|
||||
<img
|
||||
v-if="scene.poster"
|
||||
:src="scene.poster.isS3 ? `https://cdndev.traxxx.me/${scene.poster.thumbnail}` : `/media/${scene.poster.thumbnail}`"
|
||||
:style="{ 'background-image': scene.poster.isS3 ? `url(https://cdndev.traxxx.me/${scene.poster.lazy})` : `url(/media/${scene.poster.lazy})` }"
|
||||
loading="lazy"
|
||||
class="thumbnail"
|
||||
>
|
||||
</Link>
|
||||
|
||||
<div class="meta">
|
||||
<div class="channel">
|
||||
<Link
|
||||
:href="scene.channel.isIndependent || !scene.network ? `/${scene.channel.type}/${scene.channel.slug}` : `/${scene.network.type}/${scene.network.slug}`"
|
||||
class="favicon-link"
|
||||
>
|
||||
<img
|
||||
:src="scene.channel.isIndependent || !scene.network ? `/logos/${scene.channel.slug}/favicon.png` : `/logos/${scene.network.slug}/favicon.png`"
|
||||
class="favicon"
|
||||
>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
:href="`/${scene.channel.type}/${scene.channel.slug}`"
|
||||
class="nolink channel-link"
|
||||
>{{ scene.channel.name }}</Link>
|
||||
</div>
|
||||
|
||||
<time
|
||||
:datetime="scene.effectiveDate.toISOString()"
|
||||
class="date"
|
||||
>{{ format(scene.effectiveDate, 'MMM d, y') }}</time>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
:href="`/scene/${scene.id}/${scene.slug}`"
|
||||
:title="scene.title"
|
||||
target="_blank"
|
||||
class="row title nolink"
|
||||
>{{ scene.title }}</Link>
|
||||
|
||||
<ul
|
||||
class="row actors nolist"
|
||||
:title="scene.actors.map((actor) => actor.name).join(', ')"
|
||||
>
|
||||
<li
|
||||
v-for="actor in scene.actors"
|
||||
:key="`actor-${scene.id}-${actor.id}`"
|
||||
class="actor"
|
||||
>
|
||||
<Link
|
||||
:href="`/actor/${actor.id}/${actor.slug}`"
|
||||
class="nolink"
|
||||
>{{ actor.name }}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul
|
||||
class="row tags nolist"
|
||||
:title="scene.tags.map((tag) => tag.name).join(', ')"
|
||||
>
|
||||
<li
|
||||
v-for="tag in scene.tags"
|
||||
:key="`tag-${scene.id}-${tag.id}`"
|
||||
class="tag"
|
||||
>
|
||||
<Link
|
||||
:href="`/tag/${tag.slug}`"
|
||||
class="nolink"
|
||||
>{{ tag.name }}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { format } from 'date-fns';
|
||||
|
||||
defineProps({
|
||||
scene: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tile {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--background-base);
|
||||
border-radius: .25rem;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
}
|
||||
|
||||
.poster {
|
||||
display: block;
|
||||
height: 14rem;
|
||||
border-radius: .25rem .25rem 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: .4rem .5rem;
|
||||
border-radius: 0 0 .25rem .25rem;
|
||||
margin-bottom: .5rem;
|
||||
font-size: .8rem;
|
||||
color: var(--text-light);
|
||||
background: var(--shadow-strong-30);
|
||||
box-shadow: 0 0 3px var(--shadow);
|
||||
}
|
||||
|
||||
.channel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.favicon-link {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.favicon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0 .5rem .25rem .5rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
margin-bottom: .4rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.actor:hover,
|
||||
.tag:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.actors {
|
||||
height: 1rem;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.actor {
|
||||
&:not(:last-child)::after {
|
||||
content: ',\0020';
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
height: 1.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin: 0 .5rem .25rem 0;
|
||||
padding: .1rem 0;
|
||||
color: var(--shadow-strong-10);
|
||||
font-size: .75rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user