From c018f54a121226ca52290288feeec02069881ce8 Mon Sep 17 00:00:00 2001 From: DebaucheryLibrarian Date: Wed, 27 Mar 2024 00:06:03 +0100 Subject: [PATCH] Added stash menu with remove and rename. --- assets/css/inputs.css | 22 +- assets/css/theme.css | 1 + assets/img/icons/pen6.svg | 6 + assets/img/icons/pencil5.svg | 6 + assets/img/icons/pencil7.svg | 6 + components/actors/tile.vue | 2 +- components/footer/navigation.vue | 8 + components/header/header.vue | 22 +- components/movies/tile.vue | 2 +- components/scenes/tile.vue | 11 +- components/sidebar/sidebar.vue | 151 +++++++++----- components/stashes/stash.vue | 2 +- components/stashes/tile.vue | 331 +++++++++++++++++++++++++++++++ components/tiles/actor.vue | 275 ------------------------- components/tiles/gender.vue | 39 ---- config/default.cjs | 2 +- pages/users/@username/+Page.vue | 199 +++---------------- renderer/container.vue | 3 +- src/api.js | 52 ++++- src/stashes.js | 80 +++++--- src/users.js | 2 +- src/users/curate.js | 21 -- 22 files changed, 620 insertions(+), 623 deletions(-) create mode 100644 assets/img/icons/pen6.svg create mode 100644 assets/img/icons/pencil5.svg create mode 100644 assets/img/icons/pencil7.svg create mode 100644 components/stashes/tile.vue delete mode 100644 components/tiles/actor.vue delete mode 100644 components/tiles/gender.vue delete mode 100644 src/users/curate.js diff --git a/assets/css/inputs.css b/assets/css/inputs.css index 5b1d27c..4c29d84 100644 --- a/assets/css/inputs.css +++ b/assets/css/inputs.css @@ -26,7 +26,7 @@ flex-shrink: 0; align-items: stretch; box-sizing: border-box; - padding: .5rem 0 .5rem .5rem; + padding: .5rem; border: none; border-radius: .25rem; background: var(--background); @@ -57,16 +57,30 @@ } .button-label { - margin-right: .75rem; + margin-right: .25rem; } .button-submit { - color: var(--primary); + background: var(--primary); + color: var(--text-light); + justify-content: center; + + &:hover:not(:disabled) { + background: var(--primary-dark-10); + } + + &:disabled { + background: var(--shadow-weak-10); + } +} + +.button-primary { + background: var(--primary); + color: var(--text-light); justify-content: center; &:hover:not(:disabled) { background: var(--primary); - cursor: pointer; } &:disabled { diff --git a/assets/css/theme.css b/assets/css/theme.css index 05c4df1..d463677 100644 --- a/assets/css/theme.css +++ b/assets/css/theme.css @@ -1,4 +1,5 @@ :root { + --primary-dark-10: #e54485; --primary: #f65596; --primary-light-10: #f075a6; --primary-light-30: #f7c9dc; diff --git a/assets/img/icons/pen6.svg b/assets/img/icons/pen6.svg new file mode 100644 index 0000000..acb8ec1 --- /dev/null +++ b/assets/img/icons/pen6.svg @@ -0,0 +1,6 @@ + + +pen6 + + + diff --git a/assets/img/icons/pencil5.svg b/assets/img/icons/pencil5.svg new file mode 100644 index 0000000..2ac7b9a --- /dev/null +++ b/assets/img/icons/pencil5.svg @@ -0,0 +1,6 @@ + + +pencil5 + + + diff --git a/assets/img/icons/pencil7.svg b/assets/img/icons/pencil7.svg new file mode 100644 index 0000000..f7b91f6 --- /dev/null +++ b/assets/img/icons/pencil7.svg @@ -0,0 +1,6 @@ + + +pencil7 + + + diff --git a/components/actors/tile.vue b/components/actors/tile.vue index 3c1128a..007fa89 100644 --- a/components/actors/tile.vue +++ b/components/actors/tile.vue @@ -97,7 +97,7 @@ const pageStash = pageContext.pageProps.stash; // console.log(props.actor); -const favorited = ref(props.actor.stashes?.some((sceneStash) => sceneStash.primary) || false); +const favorited = ref(props.actor.stashes?.some((sceneStash) => sceneStash.isPrimary) || false); async function stash() { try { diff --git a/components/footer/navigation.vue b/components/footer/navigation.vue index 31ad9c9..11ef0de 100644 --- a/components/footer/navigation.vue +++ b/components/footer/navigation.vue @@ -68,6 +68,14 @@ const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]); font-weight: bold; font-size: .9rem; + &:first-child { + padding-left: .5rem; + } + + &:last-child { + padding-right: .5rem; + } + &:hover { cursor: pointer; diff --git a/components/header/header.vue b/components/header/header.vue index 755765b..6e68f2e 100644 --- a/components/header/header.vue +++ b/components/header/header.vue @@ -67,10 +67,13 @@ placeholder="Search" class="input" @focus="searchFocused = true" - @blur="searchFocused = false" + @blur="blurSearch" > - @@ -104,7 +107,7 @@ class="menu-button nolink" > - My profile + Profile @@ -148,7 +151,12 @@ diff --git a/components/stashes/stash.vue b/components/stashes/stash.vue index 3846e14..57658f6 100644 --- a/components/stashes/stash.vue +++ b/components/stashes/stash.vue @@ -3,7 +3,7 @@

diff --git a/components/stashes/tile.vue b/components/stashes/tile.vue new file mode 100644 index 0000000..1e96052 --- /dev/null +++ b/components/stashes/tile.vue @@ -0,0 +1,331 @@ + + + + + diff --git a/components/tiles/actor.vue b/components/tiles/actor.vue deleted file mode 100644 index acb869b..0000000 --- a/components/tiles/actor.vue +++ /dev/null @@ -1,275 +0,0 @@ - - - - - diff --git a/components/tiles/gender.vue b/components/tiles/gender.vue deleted file mode 100644 index c2b0fee..0000000 --- a/components/tiles/gender.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - diff --git a/config/default.cjs b/config/default.cjs index 9afb160..bad08b2 100755 --- a/config/default.cjs +++ b/config/default.cjs @@ -66,7 +66,7 @@ module.exports = { }, stashes: { nameLength: [2, 24], - namePattern: /^[a-zA-Z0-9_-]+$/, + namePattern: /^[a-zA-Z0-9!?$&\s_-]+$/, viewRefreshCooldown: 60, // minutes }, media: { diff --git a/pages/users/@username/+Page.vue b/pages/users/@username/+Page.vue index 2f1338a..f0ee4a8 100644 --- a/pages/users/@username/+Page.vue +++ b/pages/users/@username/+Page.vue @@ -12,7 +12,7 @@

{{ profile.username }}

- {{ formatDistanceStrict(Date.now(), profile.createdAt) }} + {{ formatDistanceStrict(Date.now(), profile.createdAt) }}
@@ -41,8 +41,9 @@
@@ -114,14 +73,13 @@ import { ref, inject } from 'vue'; import { formatDistanceStrict } from 'date-fns'; -import { get, post, patch } from '#/src/api.js'; -import events from '#/src/events.js'; +import { get, post } from '#/src/api.js'; +import StashTile from '#/components/stashes/tile.vue'; import Dialog from '#/components/dialog/dialog.vue'; const pageContext = inject('pageContext'); const profile = ref(pageContext.pageProps.profile); -const user = pageContext.user; const stashName = ref(null); const stashNameInput = ref(null); @@ -129,8 +87,8 @@ const stashNameInput = ref(null); const done = ref(true); const showStashDialog = ref(false); -function abbreviateNumber(number) { - return number?.toLocaleString('en-US', { notation: 'compact' }) || 0; +async function reloadProfile() { + profile.value = await get(`/users/${profile.value.id}`); } async function createStash() { @@ -140,56 +98,19 @@ async function createStash() { done.value = false; - try { - await post('/stashes', { - name: stashName.value, - public: false, - }); + await post('/stashes', { + name: stashName.value, + public: false, + }, { + successFeedback: `Created stash '${stashName.value}'`, + errorFeedback: `Failed to create stash '${stashName.value}'`, + appendErrorMessage: true, + }).finally(() => { done.value = true; }); - profile.value = await get(`/users/${profile.value.id}`); - showStashDialog.value = false; + showStashDialog.value = false; + stashName.value = null; - events.emit('feedback', { - type: 'success', - message: `Created stash '${stashName.value}'`, - }); - } catch (error) { - events.emit('feedback', { - type: 'error', - message: `Failed to create stash '${stashName.value}': ${error.message}`, - }); - } - - done.value = true; -} - -async function setStashPublic(stash, isPublic) { - if (done.value === false) { - return; - } - - try { - done.value = false; - - await patch(`/stashes/${stash.id}`, { public: isPublic }); - profile.value = await get(`/users/${profile.value.id}`); - - events.emit('feedback', { - type: isPublic ? 'success' : 'remove', - message: isPublic - ? `Stash '${stash.name}' set to public` - : `Stash '${stash.name}' set to private`, - }); - } catch (error) { - console.error(error); - - events.emit('feedback', { - type: 'error', - message: 'Failed to update stash', - }); - } - - done.value = true; + await reloadProfile(); } @@ -229,8 +150,10 @@ async function setStashPublic(stash, isPublic) { .age { display: flex; flex-shrink: 0; + font-size: .9rem; .icon { + width: .9rem; fill: var(--highlight-strong-20); margin-right: .75rem; transform: translateY(-1px); @@ -268,78 +191,6 @@ async function setStashPublic(stash, isPublic) { padding: 0 .5rem 1rem .5rem; } -.stash { - width: 100%; - border-radius: .25rem; - background: var(--background); - box-shadow: 0 0 3px var(--shadow-weak-30); - - &:hover { - box-shadow: 0 0 3px var(--shadow-weak-20); - } -} - -.stash-header { - display: flex; - align-items: stretch; - border-bottom: solid 1px var(--shadow-weak-30); - font-weight: bold; -} - -.icon.public { - display: flex; - height: auto; - padding: 0 .75rem; - fill: var(--shadow); - - &:hover { - cursor: pointer; - fill: var(--shadow-strong-30); - } -} - -.stash-name { - display: flex; - flex-grow: 1; - padding: .5rem; - overflow: hidden; - - .icon { - margin-left: .75rem; - } - - .icon.primary { - fill: var(--primary); - transform: translateY(1px); - } -} - -.stash-counts { - display: flex; - justify-content: space-between; -} - -.stash-count { - display: inline-flex; - align-items: center; - padding: .5rem; - flex-grow: 1; - font-size: .9rem; - - .icon { - margin-right: .5rem; - fill: var(--shadow); - } - - &:hover { - color: var(--primary); - - .icon { - fill: var(--primary); - } - } -} - .dialog-body { padding: 1rem; diff --git a/renderer/container.vue b/renderer/container.vue index 3747f32..866d0bb 100644 --- a/renderer/container.vue +++ b/renderer/container.vue @@ -164,7 +164,8 @@ onMounted(() => { background: var(--error); } - &.remove { + &.remove, + &.undo { background: var(--warn); } } diff --git a/src/api.js b/src/api.js index 766a3b9..f551a43 100644 --- a/src/api.js +++ b/src/api.js @@ -1,4 +1,5 @@ import { parse } from '@brillout/json-serializer/parse'; /* eslint-disable-line import/extensions */ +import events from '#/src/events.js'; const postHeaders = { mode: 'cors', @@ -18,39 +19,69 @@ function getQuery(data) { return `?${encodeURI(decodeURIComponent(new URLSearchParams(curatedQuery).toString()))}`; // recode so commas aren't encoded } -export async function get(path, query = {}) { +function showFeedback(isSuccess, options = {}, errorMessage) { + if (!isSuccess && typeof options.errorFeedback) { + events.emit('feedback', { + type: 'error', + message: options.appendErrorMessage && errorMessage + ? `${options.errorFeedback}: ${errorMessage}` + : options.errorFeedback, + }); + } + + if (isSuccess && options.successFeedback) { + events.emit('feedback', { + type: 'success', + message: options.successFeedback, + }); + } + + if (isSuccess && options.undoFeedback) { + events.emit('feedback', { + type: 'undo', + message: options.undoFeedback, + }); + } +} + +export async function get(path, query = {}, options = {}) { const res = await fetch(`/api${path}${getQuery(query)}`); const body = parse(await res.text()); if (res.ok) { + showFeedback(true, options); return body; } + showFeedback(false, options, body.statusMessage); throw new Error(body.statusMessage); } -export async function post(path, data, { query } = {}) { - const res = await fetch(`/api${path}${getQuery(query)}`, { +export async function post(path, data, options = {}) { + const res = await fetch(`/api${path}${getQuery(options.query)}`, { method: 'POST', body: JSON.stringify(data), ...postHeaders, }); if (res.status === 204) { + showFeedback(true, options); return null; } const body = parse(await res.text()); if (res.ok) { + showFeedback(true, options); return body; } + showFeedback(false, options, body.statusMessage); throw new Error(body.statusMessage); } -export async function patch(path, data, { query } = {}) { - const res = await fetch(`/api${path}${getQuery(query)}`, { +export async function patch(path, data, options = {}) { + const res = await fetch(`/api${path}${getQuery(options.query)}`, { method: 'PATCH', body: JSON.stringify(data), ...postHeaders, @@ -63,28 +94,33 @@ export async function patch(path, data, { query } = {}) { const body = parse(await res.text()); if (res.ok) { + showFeedback(true, options); return body; } + showFeedback(false, options, body.statusMessage); throw new Error(body.statusMessage); } -export async function del(path, { data, query } = {}) { - const res = await fetch(`/api${path}${getQuery(query)}`, { +export async function del(path, options = {}) { + const res = await fetch(`/api${path}${getQuery(options.query)}`, { method: 'DELETE', - body: JSON.stringify(data), + body: JSON.stringify(options.data), ...postHeaders, }); if (res.status === 204) { + showFeedback(true, options); return null; } const body = parse(await res.text()); if (res.ok) { + showFeedback(true, options); return body; } + showFeedback(false, options, body.statusMessage); throw new Error(body.statusMessage); } diff --git a/src/stashes.js b/src/stashes.js index 2872661..7dcb5b1 100755 --- a/src/stashes.js +++ b/src/stashes.js @@ -19,7 +19,7 @@ export function curateStash(stash, assets = {}) { id: stash.id, name: stash.name, slug: stash.slug, - primary: stash.primary, + isPrimary: stash.primary, public: stash.public, createdAt: stash.created_at, stashedScenes: stash.stashed_scenes ?? null, @@ -38,10 +38,10 @@ export function curateStash(stash, assets = {}) { function curateStashEntry(stash, user) { const curatedStashEntry = { - user_id: user.id, - name: stash.name, - slug: slugify(stash.name), - public: false, + user_id: user?.id || undefined, + name: stash.name || undefined, + slug: slugify(stash.name) || undefined, + public: stash.public ?? false, }; return curatedStashEntry; @@ -94,26 +94,30 @@ export async function fetchStashes(domain, itemId, sessionUser) { return stashes.map((stash) => curateStash(stash)); } +function verifyStashName(stash) { + if (!stash.name) { + throw new HttpError('Stash name required', 400); + } + + if (stash.name.length < config.stashes.nameLength[0]) { + throw new HttpError('Stash name is too short', 400); + } + + if (stash.name.length > config.stashes.nameLength[1]) { + throw new HttpError('Stash name is too long', 400); + } + + if (!config.stashes.namePattern.test(stash.name)) { + throw new HttpError('Stash name contains invalid characters', 400); + } +} + export async function createStash(newStash, sessionUser) { if (!sessionUser) { throw new HttpError('You are not authenthicated', 401); } - if (!newStash.name) { - throw new HttpError('Stash name required', 400); - } - - if (newStash.name.length < config.stashes.nameLength[0]) { - throw new HttpError('Stash name is too short', 400); - } - - if (newStash.name.length > config.stashes.nameLength[1]) { - throw new HttpError('Stash name is too long', 400); - } - - if (!config.stashes.namePattern.test(newStash.name)) { - throw new HttpError('Stash name contains invalid characters', 400); - } + verifyStashName(newStash); try { const stash = await knex('stashes') @@ -130,24 +134,36 @@ export async function createStash(newStash, sessionUser) { } } -export async function updateStash(stashId, newStash, sessionUser) { +export async function updateStash(stashId, updatedStash, sessionUser) { if (!sessionUser) { throw new HttpError('You are not authenthicated', 401); } - const stash = await knex('stashes') - .where({ - id: stashId, - user_id: sessionUser.id, - }) - .update(newStash) - .returning('*'); - - if (!stash) { - throw new HttpError('You are not authorized to modify this stash', 403); + if (updatedStash.name) { + verifyStashName(updatedStash); } - return curateStash(stash); + try { + const stash = await knex('stashes') + .where({ + id: stashId, + user_id: sessionUser.id, + }) + .update(curateStashEntry(updatedStash)) + .returning('*'); + + if (!stash) { + throw new HttpError('You are not authorized to modify this stash', 403); + } + + return curateStash(stash); + } catch (error) { + if (error.routine === '_bt_check_unique') { + throw new HttpError('Stash name should be unique', 409); + } + + throw error; + } } export async function removeStash(stashId, sessionUser) { diff --git a/src/users.js b/src/users.js index be038ec..ef35ac3 100755 --- a/src/users.js +++ b/src/users.js @@ -18,7 +18,7 @@ export function curateUser(user, assets = {}) { avatar: `/media/avatars/${user.id}_${user.username}.png`, createdAt: user.created_at, stashes: curatedStashes, - primaryStash: curatedStashes.find((stash) => stash.primary), + primaryStash: curatedStashes.find((stash) => stash.isPrimary), }; return curatedUser; diff --git a/src/users/curate.js b/src/users/curate.js deleted file mode 100644 index 68783e2..0000000 --- a/src/users/curate.js +++ /dev/null @@ -1,21 +0,0 @@ -export function curateUser(user, assets = {}) { - if (!user) { - return null; - } - - const curatedStashes = assets.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || []; - - const curatedUser = { - id: user.id, - username: user.username, - email: user.email, - emailVerified: user.email_verified, - identityVerified: user.identity_verified, - avatar: `/media/avatars/${user.id}_${user.username}.png`, - createdAt: user.created_at, - stashes: curatedStashes, - primaryStash: curatedStashes.find((stash) => stash.primary), - }; - - return curatedUser; -}