Replaced profile pill bar with portal page.
This commit is contained in:
6
assets/img/icons/file-video.svg
Executable file
6
assets/img/icons/file-video.svg
Executable file
@@ -0,0 +1,6 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M14.341 3.579c-0.347-0.473-0.831-1.027-1.362-1.558s-1.085-1.015-1.558-1.362c-0.806-0.591-1.197-0.659-1.421-0.659h-7.75c-0.689 0-1.25 0.561-1.25 1.25v13.5c0 0.689 0.561 1.25 1.25 1.25h11.5c0.689 0 1.25-0.561 1.25-1.25v-9.75c0-0.224-0.068-0.615-0.659-1.421v0 0zM12.271 2.729c0.48 0.48 0.856 0.912 1.134 1.271h-2.406v-2.405c0.359 0.278 0.792 0.654 1.271 1.134v0 0zM14 14.75c0 0.136-0.114 0.25-0.25 0.25h-11.5c-0.135 0-0.25-0.114-0.25-0.25v-13.5c0-0.135 0.115-0.25 0.25-0.25 0 0 7.749-0 7.75 0v3.5c0 0.276 0.224 0.5 0.5 0.5h3.5v9.75z"></path>
|
||||
<path d="M4 8h5v5h-5v-5z"></path>
|
||||
<path d="M9 10l3-2v5l-3-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 756 B |
4
assets/img/icons/profile.svg
Executable file
4
assets/img/icons/profile.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M13.5 0h-12c-0.825 0-1.5 0.675-1.5 1.5v13c0 0.825 0.675 1.5 1.5 1.5h12c0.825 0 1.5-0.675 1.5-1.5v-13c0-0.825-0.675-1.5-1.5-1.5zM13 14h-11v-12h11v12zM4 9h7v1h-7zM4 11h7v1h-7zM5 4.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5zM7.5 6h-2c-0.825 0-1.5 0.45-1.5 1v1h5v-1c0-0.55-0.675-1-1.5-1z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 488 B |
@@ -1,6 +1,8 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
@@ -31,6 +33,7 @@ onMounted(() => emit('open'));
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
v-bind="$attrs"
|
||||
@click.stop
|
||||
>
|
||||
<div class="dialog-header">
|
||||
|
||||
@@ -265,7 +265,7 @@ function blurSearch(event) {
|
||||
|
||||
<li
|
||||
v-if="user"
|
||||
class="menu-item"
|
||||
class="menu-item menu-div"
|
||||
>
|
||||
<a
|
||||
:href="`/user/${user.username}/alerts`"
|
||||
@@ -280,10 +280,29 @@ function blurSearch(event) {
|
||||
v-close-popper
|
||||
class="menu-item menu-button"
|
||||
@click="showSettings = true"
|
||||
@touchstart="showSettings = true"
|
||||
>
|
||||
<Icon icon="equalizer" />
|
||||
Settings
|
||||
<Icon icon="filter" />
|
||||
Filters
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="theme === 'dark'"
|
||||
v-close-popper
|
||||
class="menu-item menu-button menu-div"
|
||||
@click="setTheme('light')"
|
||||
>
|
||||
<Icon icon="sun3" />
|
||||
Light theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-else
|
||||
v-close-popper
|
||||
class="menu-item menu-button menu-div"
|
||||
@click="setTheme('dark')"
|
||||
>
|
||||
<Icon icon="moon" />
|
||||
Dark theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
@@ -300,28 +319,6 @@ function blurSearch(event) {
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="theme === 'dark'"
|
||||
v-close-popper
|
||||
class="menu-item menu-button"
|
||||
@click="setTheme('light')"
|
||||
@touchstart="setTheme('light')"
|
||||
>
|
||||
<Icon icon="sun3" />
|
||||
Light theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-else
|
||||
v-close-popper
|
||||
class="menu-item menu-button"
|
||||
@click="setTheme('dark')"
|
||||
@touchstart="setTheme('dark')"
|
||||
>
|
||||
<Icon icon="moon" />
|
||||
Dark theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="user"
|
||||
class="menu-button menu-item logout"
|
||||
@@ -571,6 +568,7 @@ function blurSearch(event) {
|
||||
align-items: center;
|
||||
padding: .75rem .5rem .75rem .75rem;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--glass);
|
||||
@@ -580,7 +578,6 @@ function blurSearch(event) {
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
|
||||
&:not(.logout) .icon {
|
||||
fill: var(--primary);
|
||||
@@ -592,6 +589,10 @@ function blurSearch(event) {
|
||||
}
|
||||
}
|
||||
|
||||
.menu-div {
|
||||
border-bottom: solid 1px var(--glass-weak-30);
|
||||
}
|
||||
|
||||
/*
|
||||
.favorites .icon {
|
||||
fill: var(--primary);
|
||||
|
||||
76
components/settings/filters.vue
Normal file
76
components/settings/filters.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import Cookies from 'js-cookie';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
const cookies = Cookies.withConverter({
|
||||
write: (value) => value,
|
||||
});
|
||||
|
||||
const tags = {
|
||||
'anal': 'anal',
|
||||
'anal-prolapse': 'anal prolapse',
|
||||
'extreme-insertion': 'extreme insertion (oversized dildos)',
|
||||
'pissing': 'pissing',
|
||||
'gay': 'gay',
|
||||
'transsexual': 'transsexual',
|
||||
'bisexual': 'bisexual',
|
||||
'compilation': 'compilation',
|
||||
'bts': 'behind the scenes',
|
||||
'vr': 'virtual reality',
|
||||
};
|
||||
|
||||
const storedTags = cookies.get('tags');
|
||||
const checkedTags = ref(new Set(storedTags ? JSON.parse(storedTags) : []));
|
||||
|
||||
function toggleTag(tag, isChecked) {
|
||||
if (isChecked) {
|
||||
checkedTags.value.add(tag);
|
||||
}
|
||||
else {
|
||||
checkedTags.value.delete(tag);
|
||||
}
|
||||
|
||||
cookies.set('tags', JSON.stringify(Array.from(checkedTags.value)), { expires: 400 }); // 100 years from now
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dialog-section">
|
||||
<p class="dialog-description">Check the tags that you prefer to be <strong>excluded</strong> from the results. The filter is a courtesy, and provides no guarantees.</p>
|
||||
|
||||
<ul class="tags nolist">
|
||||
<li
|
||||
v-for="(label, slug) in tags"
|
||||
:key="`tag-${slug}`"
|
||||
class="tags-item"
|
||||
>
|
||||
<label class="tag noselect">
|
||||
<Checkbox
|
||||
:value="slug"
|
||||
:checked="checkedTags.has(slug)"
|
||||
class="minus"
|
||||
@change="(checked) => toggleTag(slug, checked)"
|
||||
/>{{ label }}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tags-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
padding: .5rem 0;
|
||||
text-transform: capitalize;
|
||||
|
||||
.check-container {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
components/settings/notifications.vue
Normal file
29
components/settings/notifications.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
// import Checkbox from '#/components/form/checkbox.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dialog-section">
|
||||
<div class="setting">
|
||||
Combine e-mail alerts
|
||||
<select
|
||||
class="input"
|
||||
>
|
||||
<option>Never</option>
|
||||
<option>Daily</option>
|
||||
<option>Weekly</option>
|
||||
<option>Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.setting {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +1,66 @@
|
||||
<script setup>
|
||||
import Cookies from 'js-cookie';
|
||||
import { ref } from 'vue';
|
||||
// this component is set up as a generic settings panel, but as most account settings will be handled through the profile page, it is parading as a dedicated filters dialog
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
import Filters from '#/components/settings/filters.vue';
|
||||
import Notifications from '#/components/settings/notifications.vue';
|
||||
|
||||
const cookies = Cookies.withConverter({
|
||||
write: (value) => value,
|
||||
defineProps({
|
||||
showNavigation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const tags = {
|
||||
'anal': 'anal',
|
||||
'anal-prolapse': 'anal prolapse',
|
||||
'extreme-insertion': 'extreme insertion (oversized dildos)',
|
||||
'pissing': 'pissing',
|
||||
'gay': 'gay',
|
||||
'transsexual': 'transsexual',
|
||||
'bisexual': 'bisexual',
|
||||
'compilation': 'compilation',
|
||||
'bts': 'behind the scenes',
|
||||
'vr': 'virtual reality',
|
||||
};
|
||||
const pageContext = inject('pageContext');
|
||||
const { user } = pageContext;
|
||||
|
||||
const storedTags = cookies.get('tags');
|
||||
const checkedTags = ref(new Set(storedTags ? JSON.parse(storedTags) : []));
|
||||
|
||||
function toggleTag(tag, isChecked) {
|
||||
if (isChecked) {
|
||||
checkedTags.value.add(tag);
|
||||
}
|
||||
else {
|
||||
checkedTags.value.delete(tag);
|
||||
}
|
||||
|
||||
cookies.set('tags', JSON.stringify(Array.from(checkedTags.value)), { expires: 400 }); // 100 years from now
|
||||
}
|
||||
const activeSection = ref('filters');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog title="Settings">
|
||||
<Dialog
|
||||
title="Filters"
|
||||
class="settings-dialog"
|
||||
>
|
||||
<div class="dialog-body">
|
||||
<div class="dialog-section">
|
||||
<h3 class="dialog-heading">Filter</h3>
|
||||
<ul
|
||||
v-if="!activeSection"
|
||||
class="sections"
|
||||
>
|
||||
<li
|
||||
class="section"
|
||||
@click="activeSection = 'filters'"
|
||||
>
|
||||
<Icon icon="filter" />
|
||||
Filters
|
||||
</li>
|
||||
|
||||
<p class="dialog-description">Check the tags that you prefer to be <strong>excluded</strong> from the results. The filter is a courtesy, and provides no guarantees.</p>
|
||||
<li
|
||||
v-if="user"
|
||||
class="section"
|
||||
@click="activeSection = 'notifications'"
|
||||
>
|
||||
<Icon icon="bell2" />
|
||||
Notifications
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="tags nolist">
|
||||
<li
|
||||
v-for="(label, slug) in tags"
|
||||
:key="`tag-${slug}`"
|
||||
class="tags-item"
|
||||
>
|
||||
<label class="tag noselect">
|
||||
<Checkbox
|
||||
:value="slug"
|
||||
:checked="checkedTags.has(slug)"
|
||||
class="minus"
|
||||
@change="(checked) => toggleTag(slug, checked)"
|
||||
/>{{ label }}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-else-if="showNavigation"
|
||||
class="section-header"
|
||||
>
|
||||
<span
|
||||
class="section-back"
|
||||
@click="activeSection = null"
|
||||
><Icon icon="arrow-left3" />Back to overview</span>
|
||||
|
||||
<strong class="section-label">{{ activeSection }}</strong>
|
||||
</div>
|
||||
|
||||
<Filters v-if="activeSection === 'filters'" />
|
||||
<Notifications v-if="activeSection === 'notifications'" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -69,23 +68,74 @@ function toggleTag(tag, isChecked) {
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
width: 30rem;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tags-item {
|
||||
display: block;
|
||||
.dialog-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
padding: .5rem 0;
|
||||
text-transform: capitalize;
|
||||
.sections {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.check-container {
|
||||
margin-right: 1rem;
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: .25rem;
|
||||
font-size: 1.1rem;
|
||||
|
||||
.icon {
|
||||
fill: var(--glass-strong-10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
}
|
||||
|
||||
.section-back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: .75rem 1rem;
|
||||
|
||||
.icon {
|
||||
fill: var(--glass);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-label {
|
||||
color: var(--primary);
|
||||
text-transform: capitalize;
|
||||
padding: .75rem 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,7 +27,10 @@ function scrollHorizontal(event) {
|
||||
<div class="page">
|
||||
<div class="profile">
|
||||
<div class="profile-header">
|
||||
<div class="user">
|
||||
<a
|
||||
:href="`/user/${profile.username}/`"
|
||||
class="user nolink"
|
||||
>
|
||||
<img
|
||||
v-if="profile.avatar"
|
||||
:src="profile.avatar"
|
||||
@@ -35,13 +38,13 @@ function scrollHorizontal(event) {
|
||||
>
|
||||
|
||||
<h2 class="username ellipsis">{{ profile.username }}</h2>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<span class="age">{{ formatDistanceStrict(Date.now(), profile.createdAt) }}</span>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
v-if="profile.id === user?.id"
|
||||
v-if="!section && profile.id === user?.id"
|
||||
class="domains nobar"
|
||||
@wheel.prevent="scrollHorizontal"
|
||||
>
|
||||
@@ -49,38 +52,38 @@ function scrollHorizontal(event) {
|
||||
:href="`/user/${profile.username}/stashes`"
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'stashes' }"
|
||||
>Stashes</a>
|
||||
><Icon icon="folder-heart" />Stashes</a>
|
||||
|
||||
<a
|
||||
:href="`/user/${profile.username}/alerts`"
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'alerts' }"
|
||||
>Alerts</a>
|
||||
><Icon icon="alarm" />Alerts</a>
|
||||
|
||||
<a
|
||||
:href="`/user/${profile.username}/templates`"
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'templates' }"
|
||||
>Templates</a>
|
||||
|
||||
<a
|
||||
:href="`/user/${profile.username}/revisions/scenes`"
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'revisions' && domain === 'scenes' }"
|
||||
>Scene Revisions</a>
|
||||
|
||||
<a
|
||||
:href="`/user/${profile.username}/revisions/actors`"
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'revisions' && domain === 'actors' }"
|
||||
>Actor Revisions</a>
|
||||
><Icon icon="paste3" />Templates</a>
|
||||
|
||||
<a
|
||||
v-if="profile.isIdentityVerified"
|
||||
:href="`/user/${profile.username}/api`"
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'api' }"
|
||||
>API Keys</a>
|
||||
><Icon icon="key" />API Keys</a>
|
||||
|
||||
<a
|
||||
:href="`/user/${profile.username}/revisions/scenes`"
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'revisions' && domain === 'scenes' }"
|
||||
><Icon icon="file-video2" />Scene Revisions</a>
|
||||
|
||||
<a
|
||||
:href="`/user/${profile.username}/revisions/actors`"
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'revisions' && domain === 'actors' }"
|
||||
><Icon icon="profile" />Actor Revisions</a>
|
||||
</nav>
|
||||
|
||||
<Stashes v-if="section === 'stashes'" />
|
||||
@@ -160,6 +163,7 @@ function scrollHorizontal(event) {
|
||||
color: var(--highlight-strong-30);
|
||||
background: var(--shadow-strong-30);
|
||||
border-radius: 0 0 .5rem .5rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.user {
|
||||
@@ -193,25 +197,41 @@ function scrollHorizontal(event) {
|
||||
}
|
||||
|
||||
.domains {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(30rem, 1fr));
|
||||
gap: .5rem;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem 0;
|
||||
margin-top: .25rem;
|
||||
overflow-x: auto;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.domain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
color: var(--glass-strong-20);
|
||||
background: var(--background-dark-20);
|
||||
padding: .5rem 1rem;
|
||||
padding: 1.25rem 2rem;
|
||||
border-radius: 1rem;
|
||||
font-size: .9rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
|
||||
&.active {
|
||||
.icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
transform: translateY(-1px);
|
||||
fill: var(--glass-strong-20);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--primary);
|
||||
color: var(--text-light);
|
||||
|
||||
.icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,16 +6,17 @@ const urlMatch = match(path, { decode: decodeURIComponent });
|
||||
|
||||
export default (pageContext) => {
|
||||
const matched = urlMatch(pageContext.urlPathname);
|
||||
const isSelf = pageContext.user?.username === matched.params?.username;
|
||||
|
||||
if (matched) {
|
||||
if (![undefined, 'stashes'].includes(matched.params.domain) && pageContext.user?.username !== matched.params.username) {
|
||||
throw redirect(`/user/${matched.params.username}`);
|
||||
if (!isSelf && matched.params.section !== 'stashes') {
|
||||
throw redirect(`/user/${matched.params.username}/stashes`);
|
||||
}
|
||||
|
||||
return {
|
||||
routeParams: {
|
||||
username: matched.params.username,
|
||||
section: matched.params.section || 'stashes',
|
||||
section: matched.params.section || '',
|
||||
domain: matched.params.domain || 'scenes',
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user