diff --git a/README.md b/README.md index 50f0e001..4ec95efc 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,8 @@ LISTS_PUBLIC=false TABLE=true # Allow Markdown in item notes. Does not work with TABLE=false. Defaults to false. MARKDOWN=false +# Auto archive pledges +AUTO_ARCHIVE_PLEDGES=false ## Custom HTML Snippets # These are inserted into specific locations in the relevant page @@ -147,16 +149,26 @@ MARKDOWN=false ## OIDC Provider Details # You can configure single sign-on to your Christmas Community instance using any OIDC provider. # Once you've created a client ID and secret in your authentication provider use the below environment variables to enable single sign on. +# The values specified below are the defaults # OIDC_CLIENT_ID= # OIDC_CLIENT_SECRET= # OIDC_AUTHORIZATION_URL=https://accounts.google.com/o/oauth2/auth # OIDC_TOKEN_URL=https://oauth2.googleapis.com/token # OIDC_ISSUER=https://accounts.google.com # OIDC_PROVIDER_NAME=Google - +# OIDC_AUTO_CREATE_USER=false # Profile picture upload max size in MB UPLOAD_PFP_MAX_SIZE=5 + +# Set to false to disable pledge only users from being able to suggest items to another user's list +PLEDGE_SUGGEST=true + +# Set to false to disable all users from being able to suggest items to another users's list +SUGGESTIONS_ENABLED=true + +## Admin users can optionally edit, archive, remove, and reorder any item on any wishlist when set to true (default: false). +# ADMIN_CAN_EDIT_ALL_WISHLISTS=false ``` ## Default Profile Pictures @@ -406,4 +418,4 @@ Hi, I'm Wingy. I made this app. My website is [samwing.dev](https://samwing.dev) - + diff --git a/package.json b/package.json index e78ba79a..afd2270b 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,11 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node built/manager.js", - "dev": "nodemon --watch src -e ts,js --exec 'npm run build && node built/index.js'", + "dev": "nodemon --watch src -e ts,js,pug --exec 'npm run build && node built/index.js'", "postinstall": "node postinstall.cjs", "build": "tsc", - "release": "docker buildx build . --platform linux/amd64,linux/arm64 -t wingysam/christmas-community -t wingysam/christmas-community:$(./node_modules/node-jq/bin/jq -r .version < package.json) --push" + "release": "docker buildx build . --platform linux/amd64,linux/arm64 -t wingysam/christmas-community -t wingysam/christmas-community:$(./node_modules/node-jq/bin/jq -r .version < package.json) --push", + "lint": "eslint src && prettier . --write" }, "author": "Wingysam ", "license": "AGPL-3.0", diff --git a/src/config/index.ts b/src/config/index.ts index 5d487591..a35d5b19 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -39,7 +39,12 @@ const config = { oidcEnabled: false, pfpUploadMaxSize: process.env.UPLOAD_PFP_MAX_SIZE || 5, rootUrl: appendSlash(process.env.ROOT_URL ?? process.env.ROOT_PATH ?? '/'), + autoArchivePledges: process.env.AUTO_ARCHIVE_PLEDGES === 'true', base: '', // automatically set below + pledgeSuggest: (process.env.PLEDGE_SUGGEST === 'true'), + suggestionsEnabled: (process.env.SUGGESTIONS_ENABLED === 'true'), + oidcAutoCreateUser: process.env.OIDC_AUTO_CREATE_USER || false, + adminCanEditAllWishlists: process.env.ADMIN_CAN_EDIT_ALL_WISHLISTS|| false, } if (config.guestPassword) config.wishlist.public = false diff --git a/src/index.ts b/src/index.ts index 90749a38..6551450f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import config from './config/index.js' import PouchDB from './PouchDB.js' import logger from './logger.js' import { doDbMigrations } from './dbMigration.js' +import ensurePfp from './utils/ensurePfp.js' // from https://github.com/ai/nanoid/blob/main/url-alphabet/index.js const nanoidWithoutUnderscores = customAlphabet( @@ -98,6 +99,31 @@ if (config.oidcEnabled) { }) if (docs.docs.length === 1) { return done(null, docs.docs[0]) + } else if (config.oidcAutoCreateUser) { + // Auto-create user if enabled + const username = + profile.username || + profile.displayName || + profile.emails?.[0]?.value || + oidcId + const newUser = { + _id: username, + username, + displayName: profile.displayName || username, + emails: profile.emails || [], + oauthConnections: { oidc: oidcId }, + createdAt: new Date().toISOString(), + wishlist: [], + // Add any other required fields here + } + try { + await db.put(newUser) + await ensurePfp(db, config, username) + const createdUser = await db.get(username) + return done(null, createdUser) + } catch (err) { + return done(err) + } } else { // Handle other errors, including missing user return done(null, false, { diff --git a/src/languages/en-us.ts b/src/languages/en-us.ts index 30aed8e6..2e7eda1e 100644 --- a/src/languages/en-us.ts +++ b/src/languages/en-us.ts @@ -18,6 +18,7 @@ export const strings = { ADMIN_SETTINGS_USERS_ADD_HEADER: 'Add user', ADMIN_SETTINGS_USERS_ADD_PLACEHOLDER: 'john', ADMIN_SETTINGS_USERS_ADD_USERNAME: 'Username', + ADMIN_SETTINGS_USERS_ADD_PLEDGE_ONLY: 'Pledge only user?', ADMIN_SETTINGS_USERS_ADD_ERROR_USERNAME_EMPTY: 'Username cannot be empty.', ADMIN_SETTINGS_USERS_EDIT_DELETE_FAIL_ADMIN: 'Failed to remove: user is admin.', @@ -36,11 +37,14 @@ export const strings = { `${name} is now an admin.`, ADMIN_SETTINGS_USERS_EDIT_RENAMED_USER: 'Renamed user!', ADMIN_SETTINGS_USERS_EDIT_SAME_NAME: 'Username is same as new username.', + ADMIN_USER_EDIT_PLEDGE_ONLY: 'Pledge only', + ADMIN_USER_EDIT_PLEDGE_ONLY_BUTTON: 'Change pledge only', ADMIN_SETTINGS_USERS_EDIT: 'Edit', ADMIN_SETTINGS_USERS_HEADER: 'Users', ADMIN_SETTINGS_VERSION_INFO: 'Version Info', ADMIN_SETTINGS_TABLE_USERNAME: 'Username', ADMIN_SETTINGS_TABLE_ADMIN_USER: 'Admin', + ADMIN_SETTINGS_TABLE_PLEDGE_ONLY: 'Pledge only', ADMIN_SETTINGS_TABLE_WISHLIST_COUNT: 'Wishlist items', ADMIN_SETTINGS_TABLE_EDIT: 'Edit', ADMIN_USER_EDIT_ACCOUNT_UNCONFIRMED: "This account hasn't been confirmed.", @@ -102,6 +106,8 @@ export const strings = { LOGIN_SSO_UNLINK_FAILURE: 'Failed to unlink account', LOGIN_SSO_LINK_FAILURE_ACCOUNT_EXISTS: 'The external account is already linked to another account on this site!', + LOGIN_SSO_CREATED_USER: (username) => + `Congratulations, ${username}! You've been added and automatically logged in!`, LOGOUT_BUTTON: 'Log Out', NAVBAR_ADMIN: 'Admin Settings', NAVBAR_LOGIN: 'Log In', @@ -239,9 +245,19 @@ export const strings = { WISHLIST_URL_LABEL: `Item URL or Name (Supported Sites)`, WISHLIST_URL_PLACEHOLDER: 'https://www.amazon.com/dp/B00ZV9RDKK', WISHLIST_URL_REQUIRED: 'Item URL or Name is required', + WISHLIST_SUGGESTIONS_DISABLED : 'Suggestions are disabled.', + WISHLIST_ARCHIVE: 'Archive', + WISHLIST_RESTORE: 'Restore', + WISHLIST_ARCHIVED_ITEMS: 'Archived Items', + WISHLIST_ARCHIVE_SUCCESS: 'Successfully archived item.', + WISHLIST_RESTORE_SUCCESS: 'Successfully restored item.', + WISHLIST_ARCHIVE_GUARD: 'You are not allowed to archive this item', + WISHLIST_RESTORE_GUARD: 'You are not allowed to restore this item', + WISHLIST_DELETE_CONFIRM: 'Are you sure you want to delete this item?', + WISHLIST_ITEM_OPTIONS: 'Options', WISHLISTS_COUNTS_SELF: (name) => `${name}: ???/???`, WISHLISTS_COUNTS: (name, pledged, total) => `${name}: ${pledged}/${total}`, WISHLISTS_TITLE: `${_CC.config.siteTitle} - Wishlists`, YES: 'Yes', NO: 'No', -} as const +} as const \ No newline at end of file diff --git a/src/languages/fr-fr.ts b/src/languages/fr-fr.ts index 30bb1049..829d28a0 100644 --- a/src/languages/fr-fr.ts +++ b/src/languages/fr-fr.ts @@ -18,24 +18,30 @@ export const strings = { ADMIN_SETTINGS_USERS_ADD_HEADER: 'Ajouter un utilisateur', ADMIN_SETTINGS_USERS_ADD_PLACEHOLDER: 'jean', ADMIN_SETTINGS_USERS_ADD_USERNAME: "Nom d'utilisateur", - ADMIN_SETTINGS_USERS_ADD_ERROR_USERNAME_EMPTY: "Le nom d'utilisateur ne peut pas être vide.", + ADMIN_SETTINGS_USERS_ADD_ERROR_USERNAME_EMPTY: + "Le nom d'utilisateur ne peut pas être vide.", ADMIN_SETTINGS_USERS_EDIT_DELETE_FAIL_ADMIN: "Échec de la suppression : l'utilisateur est un administrateur.", ADMIN_SETTINGS_USERS_EDIT_DELETE_SUCCESS: (name) => `Utilisateur supprimé avec succès ${name}`, - ADMIN_SETTINGS_USERS_EDIT_DEMOTE_NOT_ADMIN: "l'utilisateur n'est pas un administrateur", - ADMIN_SETTINGS_USERS_EDIT_DEMOTE_SELF: 'Vous ne pouvez pas vous supprimer vous même.', + ADMIN_SETTINGS_USERS_EDIT_DEMOTE_NOT_ADMIN: + "l'utilisateur n'est pas un administrateur", + ADMIN_SETTINGS_USERS_EDIT_DEMOTE_SELF: + 'Vous ne pouvez pas vous supprimer vous même.', ADMIN_SETTINGS_USERS_EDIT_DEMOTE_SUCCESS: (name) => `${name} n'est plus un administrateur.`, ADMIN_SETTINGS_USERS_EDIT_IMPERSONATE_SUCCESS: (name) => `Vous êtes maintenant ${name}.`, - ADMIN_SETTINGS_USERS_EDIT_NO_USERNAME_PROVIDED: "Aucun nom d'utilisateur fourni", - ADMIN_SETTINGS_USERS_EDIT_PROMOTE_ALREADY_ADMIN: 'cet utilisateur est déjà administrateur', + ADMIN_SETTINGS_USERS_EDIT_NO_USERNAME_PROVIDED: + "Aucun nom d'utilisateur fourni", + ADMIN_SETTINGS_USERS_EDIT_PROMOTE_ALREADY_ADMIN: + 'cet utilisateur est déjà administrateur', ADMIN_SETTINGS_USERS_EDIT_PROMOTE_DEMOTE_NOT_FOUND: 'Utilisateur non trouvé.', ADMIN_SETTINGS_USERS_EDIT_PROMOTE_SUCCESS: (name) => `${name} est maintenant un administrateur.`, ADMIN_SETTINGS_USERS_EDIT_RENAMED_USER: 'Utilisateur renommé!', - ADMIN_SETTINGS_USERS_EDIT_SAME_NAME: "L'ancien nom d'utilisateur est le même que le nouveau nom d'utilisateur.", + ADMIN_SETTINGS_USERS_EDIT_SAME_NAME: + "L'ancien nom d'utilisateur est le même que le nouveau nom d'utilisateur.", ADMIN_SETTINGS_USERS_EDIT: 'Modifier', ADMIN_SETTINGS_USERS_HEADER: 'Utilisateurs', ADMIN_SETTINGS_VERSION_INFO: 'Informations sur la version', @@ -45,7 +51,8 @@ export const strings = { ADMIN_SETTINGS_TABLE_EDIT: 'Modifier', ADMIN_USER_EDIT_ACCOUNT_UNCONFIRMED: "Ce compte n'a pas été confirmé.", ADMIN_USER_EDIT_ADMIN_ISADMIN: (name) => `${name} est un administrateur.`, - ADMIN_USER_EDIT_ADMIN_NOTADMIN: (name) => `${name} n'est pas un administrateur.`, + ADMIN_USER_EDIT_ADMIN_NOTADMIN: (name) => + `${name} n'est pas un administrateur.`, ADMIN_USER_EDIT_ADMIN: 'Admin', ADMIN_USER_EDIT_CHANGE_NAME: 'Changer de nom', ADMIN_USER_EDIT_CHANGE_USERNAME: "Changer de nom d'utilisateur", @@ -55,9 +62,11 @@ export const strings = { ADMIN_USER_EDIT_DELETE_USER: (name) => `Supprimer l'utilisateur ${name}`, ADMIN_USER_EDIT_DEMOTE_SELF: 'Vous ne pouvez pas vous rétrograder', ADMIN_USER_EDIT_DEMOTE: (name) => `Rétrograder ${name}`, - ADMIN_USER_EDIT_EDITING_USER: (name) => `Modification de l'utilisateur "${name}"`, + ADMIN_USER_EDIT_EDITING_USER: (name) => + `Modification de l'utilisateur "${name}"`, ADMIN_USER_EDIT_GENERATE_NEW_LINK: 'Générer un nouveau lien', - ADMIN_USER_EDIT_IMPERSONATE_BUTTON: (name) => `Se connecter en tant que ${name}`, + ADMIN_USER_EDIT_IMPERSONATE_BUTTON: (name) => + `Se connecter en tant que ${name}`, ADMIN_USER_EDIT_IMPERSONATE_HEADER: 'se faire passer pour', ADMIN_USER_EDIT_LINK_EXPIRY_FUTURE: (fromNow) => `Le lien suivant expire le ${fromNow}`, // fromNow is localized by moment @@ -71,9 +80,12 @@ export const strings = { ADMIN_USER_EDIT_RESET_PASSWORD_HASLINK: 'Il y a un lien de réinitialisation de mot de passe pour cet utilisateur.', ADMIN_USER_EDIT_RESET_PASSWORD_HEADER: 'Réinitialiser le mot de passe', - ADMIN_USER_EDIT_RESET_PASSWORD_LINK_CANCEL: 'Supprimer le lien de réinitialisation du mot de passe', - ADMIN_USER_EDIT_RESET_PASSWORD_LINK_CREATE: 'Créer un lien de réinitialisation de mot de passe', - ADMIN_USER_EDIT_RESET_PASSWORD_LINK_REFRESH: 'Actualiser le lien de réinitialisation du mot de passe', + ADMIN_USER_EDIT_RESET_PASSWORD_LINK_CANCEL: + 'Supprimer le lien de réinitialisation du mot de passe', + ADMIN_USER_EDIT_RESET_PASSWORD_LINK_CREATE: + 'Créer un lien de réinitialisation de mot de passe', + ADMIN_USER_EDIT_RESET_PASSWORD_LINK_REFRESH: + 'Actualiser le lien de réinitialisation du mot de passe', ADMIN_USER_EDIT_USERNAME: "Nom d'utilisateur", BACK_BUTTON: 'Retour', CONFIRM_ACCOUNT_EXPIRED: @@ -136,7 +148,8 @@ export const strings = { PROFILE_PASSWORD_REQUIRED_NEW: 'Un nouveau mot de passe est requis', PROFILE_PASSWORD_REQUIRED_OLD: "L'ancien mot de passe est requis", PROFILE_PASSWORD_SUCCESS: 'Les modifications on été enregistré avec succès!', - PROFILE_PASSWORD_TITLE: (name) => `Paramètres de profil - Mot de passe - ${name}`, + PROFILE_PASSWORD_TITLE: (name) => + `Paramètres de profil - Mot de passe - ${name}`, PROFILE_PHONE_MODEL: 'Modèle de téléphone', PROFILE_RING_SIZE: 'Taille de bague', PROFILE_SAVE_PFP_DISABLED: 'Les photos de profil sont désactivées.', @@ -197,10 +210,12 @@ export const strings = { WISHLIST_ADDED_BY_GUEST: 'Utilisateur Invité', WISHLIST_ADDED_BY: 'Ajouté par', WISHLIST_ADDED_ITEM_TO_OWN_WISHLIST: "Article ajouté à la listes d'envies.", - WISHLIST_CONFLICT: 'Les éléments ont été ajoutés trop rapidement. Veuillez réessayer.', + WISHLIST_CONFLICT: + 'Les éléments ont été ajoutés trop rapidement. Veuillez réessayer.', WISHLIST_DELETE: 'Supprimer', WISHLIST_EDIT_ITEM: "Modifier l'article", - WISHLIST_FETCH_FAIL: "Échec de la récupération de la liste de souhaits : l'utilisateur existe-t-il ?", + WISHLIST_FETCH_FAIL: + "Échec de la récupération de la liste de souhaits : l'utilisateur existe-t-il ?", WISHLIST_IMAGE: 'Image', WISHLIST_ITEM_MISSING: "Impossible de trouver l'article", WISHLIST_MOVE_DOWN: 'Descendre', diff --git a/src/routes/adminSettings/index.js b/src/routes/adminSettings/index.js index 6ec88624..8810e07e 100644 --- a/src/routes/adminSettings/index.js +++ b/src/routes/adminSettings/index.js @@ -38,10 +38,10 @@ export default function ({ db, ensurePfp }) { .then((docs) => { res.render('adminSettings', { add_user_error: _CC.lang( - 'ADMIN_SETTINGS_USERS_ADD_ERROR_USERNAME_EMPTY', + 'ADMIN_SETTINGS_USERS_ADD_ERROR_USERNAME_EMPTY' ), title: _CC.lang('ADMIN_SETTINGS_HEADER'), - users: docs.rows, + users: docs.rows }) }) .catch((err) => { @@ -49,13 +49,17 @@ export default function ({ db, ensurePfp }) { }) } + const pledgeOnlyValue = req.body.newPledgeOnlyUser + const pledgeOnly = pledgeOnlyValue === 'on' + await db.put({ _id: username, admin: false, wishlist: [], + pledgeOnly: pledgeOnly, signupToken: nanoid(SECRET_TOKEN_LENGTH), - expiry: new Date().getTime() + SECRET_TOKEN_LIFETIME, + expiry: new Date().getTime() + SECRET_TOKEN_LIFETIME }) await ensurePfp(username) @@ -156,6 +160,23 @@ export default function ({ db, ensurePfp }) { } }) + router.post('/edit/pledgeOnly/:userToChange', verifyAuth(), async (req, res) => { + + const newPledgeValue = req.body.editPledgeOnlyUser + const newPledge = newPledgeValue === 'on' + + const userDoc = await db.get(req.params.userToChange) + userDoc.pledgeOnly = newPledge + try { + await db.put(userDoc) + await _CC.wishlistManager.clearCache() + return res.redirect(`/admin-settings/edit/${req.params.userToChange}`) + } catch (error) { + req.flash('error', error.message) + return res.redirect(`/admin-settings/edit/${req.params.userToChange}`) + } + }) + router.post( '/edit/impersonate/:userToEdit', verifyAuth(), @@ -304,4 +325,4 @@ export default function ({ db, ensurePfp }) { }) return router -} +} \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index efe7e9d8..f4608429 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -2,6 +2,7 @@ import publicRoute from '../middlewares/publicRoute.js' import express from 'express' import path from 'path' import fs from 'fs/promises' +import ensurePfp from '../utils/ensurePfp.js' import Api from './api/index.js' import Setup from './setup/index.js' @@ -17,27 +18,10 @@ import ManifestJson from './manifest.json/index.js' import OIDC from './oidc/index.js' export default ({ db, config }) => { - async function ensurePfp(username) { - if (!config.pfp) return - const user = await db.get(username) - if (user.pfp) return - - const { rows } = await db.allDocs({ include_docs: true }) - - const unfilteredPool = await fs.readdir('src/static/img/default-pfps') - const filteredPool = unfilteredPool.filter( - (file) => !rows.find((row) => row.doc.pfp?.default === file), - ) - const pool = filteredPool.length ? filteredPool : unfilteredPool - - user.pfp = { default: _CC._.sample(pool) } - await db.put(user) - } - ;(async () => { const { rows } = await db.allDocs({ include_docs: true }) for (const row of rows) { - await ensurePfp(row.id) + await ensurePfp(db, config, row.id) } })() @@ -63,7 +47,7 @@ export default ({ db, config }) => { router.use('/api', Api()) - router.use('/setup', Setup({ db, ensurePfp })) + router.use('/setup', Setup({ db, ensurePfp: (username) => ensurePfp(db, config, username) })) router.use('/login', Login({ config })) router.use('/logout', Logout()) @@ -73,9 +57,9 @@ export default ({ db, config }) => { router.use('/wishlist', Wishlist(db)) router.use('/supported-sites', SupportedSites()) - router.use('/profile', Profile({ db, config, ensurePfp })) + router.use('/profile', Profile({ db, config, ensurePfp: (username) => ensurePfp(db, config, username) })) - router.use('/admin-settings', AdminSettings({ db, ensurePfp })) + router.use('/admin-settings', AdminSettings({ db, ensurePfp: (username) => ensurePfp(db, config, username) })) router.use('/manifest.json', ManifestJson({ config })) diff --git a/src/routes/setup/index.js b/src/routes/setup/index.js index ed66ae31..48d23582 100644 --- a/src/routes/setup/index.js +++ b/src/routes/setup/index.js @@ -30,6 +30,7 @@ export default function ({ db, ensurePfp }) { admin: true, wishlist: [], oauthConnections: {}, + pledgeOnly: false }) resolve() }, diff --git a/src/routes/wishlist/index.js b/src/routes/wishlist/index.js index 6c2b4659..9fb36c83 100644 --- a/src/routes/wishlist/index.js +++ b/src/routes/wishlist/index.js @@ -12,11 +12,15 @@ const DOMPurify = createDOMPurify(window) const totals = (wishlist) => { let unpledged = 0 let pledged = 0 + let active = 0 + let archived = 0 wishlist.forEach((wishItem) => { if (wishItem.pledgedBy) pledged += 1 else unpledged += 1 + if (wishItem.archived) archived += 1 + else active += 1 }) - return { unpledged, pledged } + return { unpledged, pledged, active, archived } } export default function (db) { @@ -31,6 +35,7 @@ export default function (db) { if (row.doc.admin) return res.redirect(`/wishlist/${row.doc._id}`) } } + console.log(docs) res.render('wishlists', { title: _CC.lang('WISHLISTS_TITLE'), users: docs.rows, @@ -106,7 +111,6 @@ export default function (db) { } catch (error) { req.flash('error', `${error}`) } - res.redirect(`/wishlist/${req.params.user}`) }) @@ -154,7 +158,8 @@ export default function (db) { const isOwnWishlist = req.user._id === wishlist.username const addedByUser = item.addedBy === req.user._id - if (!isOwnWishlist && !addedByUser) { + const isAdmin = req.user.admin && _CC.config.adminCanEditAllWishlists; + if (!isOwnWishlist && !addedByUser && !isAdmin) { throw new Error(_CC.lang('WISHLIST_REMOVE_GUARD')) } @@ -173,7 +178,8 @@ export default function (db) { verifyAuth(), async (req, res) => { try { - if (req.user._id !== req.params.user) { + const isAdmin = req.user.admin && _CC.config.adminCanEditAllWishlists; + if (req.user._id !== req.params.user && !isAdmin) { throw new Error(_CC.lang('WISHLIST_MOVE_GUARD')) } @@ -218,7 +224,8 @@ export default function (db) { const isOwnWishlist = req.user._id === req.params.user const addedByUser = req.user._id === item.addedBy - if (!isOwnWishlist && !addedByUser) { + const isAdmin = req.user.admin && _CC.config.adminCanEditAllWishlists; + if (!isOwnWishlist && !addedByUser && !isAdmin) { throw new Error(_CC.lang('NOTE_GUARD')) } @@ -253,5 +260,49 @@ export default function (db) { res.redirect(`/wishlist/${req.params.user}/note/${req.params.id}`) }) + router.post('/:user/archive/:itemId', verifyAuth(), async (req, res) => { + try { + const wishlist = await wishlistManager.get(req.params.user) + const item = await wishlist.get(req.params.itemId) + + const isOwnWishlist = req.user._id === wishlist.username + const addedByUser = item.addedBy === req.user._id + const isAdmin = req.user.admin && _CC.config.adminCanEditAllWishlists; + if (!isOwnWishlist && !addedByUser && !isAdmin) { + throw new Error(_CC.lang('WISHLIST_ARCHIVE_GUARD')) + } + + await wishlist.archive(item.id) + + req.flash('success', _CC.lang('WISHLIST_ARCHIVE_SUCCESS')) + } catch (error) { + req.flash('error', `${error}`) + } + + res.redirect(`/wishlist/${req.params.user}`) + }) + + router.post('/:user/restore/:itemId', verifyAuth(), async (req, res) => { + try { + const wishlist = await wishlistManager.get(req.params.user) + const item = await wishlist.get(req.params.itemId) + + const isOwnWishlist = req.user._id === wishlist.username + const addedByUser = item.addedBy === req.user._id + const isAdmin = req.user.admin && _CC.config.adminCanEditAllWishlists; + if (!isOwnWishlist && !addedByUser && !isAdmin) { + throw new Error(_CC.lang('WISHLIST_RESTORE_GUARD')) + } + + await wishlist.restore(item.id) + + req.flash('success', _CC.lang('WISHLIST_RESTORE_SUCCESS')) + } catch (error) { + req.flash('error', `${error}`) + } + + res.redirect(`/wishlist/${req.params.user}`) + }) + return router } diff --git a/src/static/css/main.css b/src/static/css/main.css index 2897dd41..623a8379 100644 --- a/src/static/css/main.css +++ b/src/static/css/main.css @@ -110,5 +110,62 @@ img.logo-image { } .print-gift:last-child { - margin-bottom: 0; + margin-bottom: 0; +} + +a.disabled { + pointer-events: none; + cursor: default; +} + +summary { + font-weight: bold; + font-size: 18px; +} + +summary:hover { + cursor: pointer; +} + +.hamburger { + font-size: 24px; + background: none; + border: none; + cursor: pointer; +} + +.hidden-menu { + display: none; + margin-top: 10px; +} + +.hidden-menu button { + display: block; + margin: 5px 0; +} + +.hidden-menu.active { + display: block; +} + +button.button.dropdown-item.is-text { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + width: 100%; + text-align: left; + padding-right: 10px; /* extra Spacing to push icons to the right a bunch */ + text-decoration: none; +} + +/* Ensure the text stays left-aligned */ +button.button.dropdown-item.is-text span { + grid-column: 1; + text-align: left; +} + +/* Ensure the icon is pulled to the right */ +button.button.dropdown-item.is-text i { + grid-column: 3; + margin-left: auto; } diff --git a/src/static/js/wishlist.js b/src/static/js/wishlist.js index 1f04a4c5..79806e3f 100644 --- a/src/static/js/wishlist.js +++ b/src/static/js/wishlist.js @@ -163,3 +163,8 @@ document.addEventListener('DOMContentLoaded', () => { updateArrow('price', sortBy) }) }) + +function _toggleMenu(button) { + const menu = button.nextElementSibling + menu.classList.toggle('active') +} diff --git a/src/structures/Wishlist.js b/src/structures/Wishlist.js index 3866fa3c..886dd115 100644 --- a/src/structures/Wishlist.js +++ b/src/structures/Wishlist.js @@ -97,6 +97,10 @@ export class Wishlist { async pledge(id, user) { const item = await this.get(id) item.pledgedBy = user + + if (_CC.config.autoArchivePledges) { + item.archived = true + } await this.save() } @@ -183,6 +187,18 @@ export class Wishlist { await this.save() } + + async archive(id) { + const item = await this.get(id) + item.archived = true + await this.save() + } + + async restore(id) { + const item = await this.get(id) + item.archived = false + await this.save() + } } function parseURL(string) { diff --git a/src/utils/ensurePfp.js b/src/utils/ensurePfp.js new file mode 100644 index 00000000..fa3bc92c --- /dev/null +++ b/src/utils/ensurePfp.js @@ -0,0 +1,24 @@ +import fs from 'fs/promises' + +/** + * Ensures a user has a profile picture. If not, assigns a random default. + * @param {object} db - The user database (PouchDB instance) + * @param {object} config - The app config + * @param {string} username - The username to ensure pfp for + * @returns {Promise} + */ +export default async function ensurePfp(db, config, username) { + if (!config.pfp) return + const user = await db.get(username) + if (user.pfp) return + + const { rows } = await db.allDocs({ include_docs: true }) + const unfilteredPool = await fs.readdir('src/static/img/default-pfps') + const filteredPool = unfilteredPool.filter( + (file) => !rows.find((row) => row.doc.pfp?.default === file), + ) + const pool = filteredPool.length ? filteredPool : unfilteredPool + + user.pfp = { default: _CC._.sample(pool) } + await db.put(user) +} diff --git a/src/views/admin-user-edit.pug b/src/views/admin-user-edit.pug index c7869334..0976aa00 100644 --- a/src/views/admin-user-edit.pug +++ b/src/views/admin-user-edit.pug @@ -44,6 +44,15 @@ block content .field .control input.button.is-primary(type='submit' value=lang('ADMIN_USER_EDIT_CHANGE_USERNAME')) + .column.is-narrow + h2= lang('ADMIN_USER_EDIT_PLEDGE_ONLY') + form(action=`${_CC.config.base}admin-settings/edit/pledgeOnly/${user._id}`, method='POST') + .field + .control + input(type='checkbox', name='editPledgeOnlyUser', checked=user.pledgeOnly) + .field + .control + input.input.button(type='submit', value=lang('ADMIN_USER_EDIT_PLEDGE_ONLY_BUTTON'), style='margin-top: 1em;') .column.is-narrow h2= lang('ADMIN_USER_EDIT_ADMIN') //- Yes, ternary exists, but I think the code is cleaner with a more "naive" style :) diff --git a/src/views/adminSettings.pug b/src/views/adminSettings.pug index e88428bc..be95f330 100644 --- a/src/views/adminSettings.pug +++ b/src/views/adminSettings.pug @@ -6,6 +6,7 @@ block content thead th=lang('ADMIN_SETTINGS_TABLE_USERNAME') th=lang('ADMIN_SETTINGS_TABLE_ADMIN_USER') + th=lang('ADMIN_SETTINGS_TABLE_PLEDGE_ONLY') th=lang('ADMIN_SETTINGS_TABLE_WISHLIST_COUNT') th=lang('ADMIN_SETTINGS_TABLE_EDIT') tbody @@ -13,6 +14,7 @@ block content tr(id=user.id) td=user.id td=user.doc.admin ? lang('YES') : lang('NO') + td=user.doc.pledgeOnly ? lang('YES') : lang('NO') td=Object.keys(user.doc.wishlist).length td a(href=`${_CC.config.base}admin-settings/edit/${user.id}`) @@ -30,6 +32,10 @@ block content input.input(type='text', name='newUserUsername', placeholder=lang('ADMIN_SETTINGS_USERS_ADD_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-user + .field + label.label= lang('ADMIN_SETTINGS_USERS_ADD_PLEDGE_ONLY') + .control.has-icons-left + input(type='checkbox', name='newPledgeOnlyUser') .field .control input.button.is-primary(type='submit' value=lang('ADMIN_SETTINGS_USERS_ADD_BUTTON')) diff --git a/src/views/includes/navbar.pug b/src/views/includes/navbar.pug index 092c8285..1c42ac01 100644 --- a/src/views/includes/navbar.pug +++ b/src/views/includes/navbar.pug @@ -30,7 +30,8 @@ nav.navbar.is-fixed-top(role='navigation', aria-label='main navigation',style='b .navbar-item.has-dropdown.is-hoverable a.navbar-link= req.user._id .navbar-dropdown - +navBarLink(`${_CC.config.base}wishlist/${req.user._id}`, lang('NAVBAR_WISHLIST')) + if !req.user.pledgeOnly + +navBarLink(`${_CC.config.base}wishlist/${req.user._id}`, lang('NAVBAR_WISHLIST')) +navBarLink(`${_CC.config.base}profile`, lang('NAVBAR_PROFILE')) if req.user.admin +navBarLink(`${_CC.config.base}admin-settings`, lang('NAVBAR_ADMIN')) diff --git a/src/views/profile.pug b/src/views/profile.pug index 13fe9064..a2b81fa6 100644 --- a/src/views/profile.pug +++ b/src/views/profile.pug @@ -44,21 +44,22 @@ block content i.fas.fa-upload span= lang('PROFILE_PFP_UPLOAD') input.input.is-hidden#profilePicture(type="file" name="profilePicture", accept="image/png, image/jpeg, image/jpg") - h2= lang('PROFILE_SHARED_INFORMATION') - form(action=`${_CC.config.base}profile/info`, method='POST') - .columns.is-multiline.is-mobile - .column.is-narrow - +sharedInfoProp('shoeSize', 'PROFILE_SHOE_SIZE') - +sharedInfoProp('ringSize', 'PROFILE_RING_SIZE') - +sharedInfoProp('dressSize', 'PROFILE_DRESS_SIZE') - .column.is-narrow - +sharedInfoProp('sweaterSize', 'PROFILE_SWEATER_SIZE') - +sharedInfoProp('shirtSize', 'PROFILE_SHIRT_SIZE') - +sharedInfoProp('pantsSize', 'PROFILE_PANTS_SIZE') - .column.is-narrow - +sharedInfoProp('coatSize', 'PROFILE_COAT_SIZE') - +sharedInfoProp('hatSize', 'PROFILE_HAT_SIZE') - +sharedInfoProp('phoneModel', 'PROFILE_PHONE_MODEL') + if !req.user.pledgeOnly + h2= lang('PROFILE_SHARED_INFORMATION') + form(action=`${_CC.config.base}profile/info`, method='POST') + .columns.is-multiline.is-mobile + .column.is-narrow + +sharedInfoProp('shoeSize', 'PROFILE_SHOE_SIZE') + +sharedInfoProp('ringSize', 'PROFILE_RING_SIZE') + +sharedInfoProp('dressSize', 'PROFILE_DRESS_SIZE') + .column.is-narrow + +sharedInfoProp('sweaterSize', 'PROFILE_SWEATER_SIZE') + +sharedInfoProp('shirtSize', 'PROFILE_SHIRT_SIZE') + +sharedInfoProp('pantsSize', 'PROFILE_PANTS_SIZE') + .column.is-narrow + +sharedInfoProp('coatSize', 'PROFILE_COAT_SIZE') + +sharedInfoProp('hatSize', 'PROFILE_HAT_SIZE') + +sharedInfoProp('phoneModel', 'PROFILE_PHONE_MODEL') h2= lang('PROFILE_SECURITY') a.button.is-primary(href=`${_CC.config.base}profile/password`) diff --git a/src/views/wishlist.pug b/src/views/wishlist.pug index 47d8a1b9..cf917cf8 100644 --- a/src/views/wishlist.pug +++ b/src/views/wishlist.pug @@ -6,6 +6,240 @@ mixin sharedInfoProp(key, label) th= lang(label) td= sharedInfo[key] +mixin wishlistTable(items, archived) + .box + table.table.has-mobile-cards + thead + th # + th(style='width: 10%;')= lang('WISHLIST_IMAGE') + th(style='width: 20%;')= lang('WISHLIST_NAME') + th(style='width: 30%;')= lang('WISHLIST_NOTE') + th(style='width: 10%;')= lang('WISHLIST_PRICE') + th= lang('WISHLIST_ADDED_BY') + if !(req.params.user === req.user._id) + th(style='width: 10%;')= lang('WISHLIST_PLEDGE') + if req.params.user === req.user._id || (req.user.admin && _CC.config.adminCanEditAllWishlists) + th(style='width: 10%;')= lang('WISHLIST_ITEM_OPTIONS') + tbody + each item, index in items + tr(id=item.id) + td.rank= index + 1 + td + figure(style='width: 100%; margin: 0;') + img(src=item.image, style='width: 100%; max-height: 20em; object-fit: contain;') + if item.url + td.ugc(data-label=lang('WISHLIST_NAME')) + if item.pledgedBy && item.addedBy != req.user._id + strike + a( + href=item.url, + rel='noopener noreferrer', + target='_blank' + )= (item.name ? item.name : item.url) + else + a( + href=item.url, + rel='noopener noreferrer', + target='_blank' + )= (item.name ? item.name : item.url) + else + td.ugc(data-label='Name') + if item.pledgedBy && item.addedBy != req.user._id + strike + span= item.name + else + span= item.name + if _CC.config.wishlist.note.markdown + td.ugc(data-label=lang('WISHLIST_NOTE')) + div!= compiledNotes[item.id] + else + td.ugc(data-label=lang('WISHLIST_NOTE'))= item.note + td.ugc(data-label=lang('WISHLIST_PRICE'))= item.price + if item.addedBy === '_CCUNKNOWN' + td.ugc(data-label=lang('WISHLIST_ADDED_BY'))= lang('WISHLIST_ADDED_BY_GUEST') + else + td.ugc(data-label=lang('WISHLIST_ADDED_BY'))= item.addedBy + if !(req.params.user === req.user._id) + td(data-label=lang('WISHLIST_PLEDGE')) + if req.params.user !== req.user._id && !item.pledgedBy && !item.archived + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/pledge/${item.id}`) + .field.inline + .control.inline + input.inline.button.is-primary(type='submit' value=lang('WISHLIST_PLEDGE_ITEM')) + if item.pledgedBy === req.user._id + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/unpledge/${item.id}`) + .field.inline + .control.inline + input.inline.button(type='submit' value=lang('WISHLIST_UNPLEDGE')) + if item.pledgedBy && item.pledgedBy !== req.user._id + if item.pledgedBy === '_CCUNKNOWN' + span.ugc=lang('WISHLIST_PLEDGED_GUEST') + else + span.ugc=lang('WISHLIST_PLEDGED', item.pledgedBy) + if req.params.user === req.user._id || (req.user.admin && _CC.config.adminCanEditAllWishlists) + td(data-label=lang('WISHLIST_ITEM_OPTIONS')) + div.dropdown.is-right.is-hoverable + div.dropdown-trigger + button.button.is-primary( + aria-haspopup='true', + aria-controls=`dropdown-menu${index}` + ) + span.icon + i.fas.fa-cog + div.dropdown-menu(id=`dropdown-menu${index}`, role='menu') + div.dropdown-content + form.inline(method='GET', action=`${_CC.config.base}wishlist/${req.params.user}/note/${item.id}`) + button.button.dropdown-item.is-text( + type='submit' + ) + span= lang('WISHLIST_EDIT_ITEM') + i.far.fa-edit + + if !archived + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/up/${item.id}`) + button.button.dropdown-item.is-text( + type='submit', + disabled=index === 0 + ) + span= lang('WISHLIST_MOVE_ITEM_UP') + i.fas.fa-arrow-up + + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/down/${item.id}`) + button.button.dropdown-item.is-text( + type='submit', + disabled=index === items.length - 1 + ) + span= lang('WISHLIST_MOVE_ITEM_DOWN') + i.fas.fa-arrow-down + + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/top/${item.id}`) + button.button.dropdown-item.is-text( + type='submit', + disabled=index === 0 + ) + span= lang('WISHLIST_MOVE_ITEM_TOP') + i.fas.fa-angle-double-up + + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/bottom/${item.id}`) + button.button.dropdown-item.is-text( + type='submit', + disabled=index === items.length - 1 + ) + span= lang('WISHLIST_MOVE_ITEM_BOTTOM') + i.fas.fa-angle-double-down + + hr.dropdown-divider + if !archived + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/archive/${item.id}`) + button.button.dropdown-item.is-text.has-text-danger( + type='submit' + ) + span= lang('WISHLIST_ARCHIVE') + i.fas.fa-archive + else + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/restore/${item.id}`) + button.button.dropdown-item.is-text.has-text-danger( + type='submit' + ) + span= lang('WISHLIST_RESTORE') + i.fas.fa-restore + + +mixin wishlistBox(items, archived) + each item, index in items + if req.user._id === item.addedBy || req.params.user !== req.user._id || (req.user.admin && _CC.config.adminCanEditAllWishlists) + .box + if item.price + .tag + i.fas.fa-tag(style='margin-right: .5em;') + span= item.price + span.overflowWrap + if item.price + span= ' ' + if item.url + if item.pledgedBy && item.addedBy != req.user._id + strike + a( + href=item.url, + rel='noopener noreferrer', + target='_blank' + )= (item.name ? item.name : item.url) + else + a( + href=item.url, + rel='noopener noreferrer', + target='_blank' + )= (item.name ? item.name : item.url) + else + if item.pledgedBy && item.addedBy != req.user._id + strike + span= item.name + else + span= item.name + hr + if item.note || item.image + .columns + if item.image + .column + figure(style='width: 100%; padding: 2em;') + img(src=item.image, style='width: 100%;') + .column.is-three-quarters + if item.note + textarea.textarea.has-fixed-size( + style='height: 100%;' + readonly + )= item.note + if item.addedBy === '_CCUNKNOWN' + span.overflowWrap=lang('WISHLIST_ADDED_BY', lang('WISHLIST_ADDED_BY_GUEST')) + else + span.overflowWrap=lang('WISHLIST_ADDED_BY', item.addedBy) + hr + div + if req.params.user !== req.user._id && !item.pledgedBy + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/pledge/${item.id}`) + .field.inline + .control.inline + input.inline.button.is-primary(type='submit' value=lang('WISHLIST_PLEDGE')) + if item.pledgedBy === req.user._id + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/unpledge/${item.id}`) + .field.inline + .control.inline + input.inline.button(type='submit' value=lang('WISHLIST_UNPLEDGE')) + if req.user._id === req.params.user || (req.user.admin && _CC.config.adminCanEditAllWishlists) + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/remove/${item.id}`, onsubmit=`return confirm("${lang('WISHLIST_DELETE_CONFIRM')}")`) + .field.inline + .control.inline + input.inline.button.is-danger(type='submit' value=lang('WISHLIST_DELETE')) + form.inline(method='GET', action=`${_CC.config.base}wishlist/${req.params.user}/note/${item.id}`) + .field.inline + .control.inline + input.inline.button(type='submit', value=lang('WISHLIST_EDIT_ITEM')) + if !archived + if index !== 0 + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/up/${item.id}`) + .field.inline + .control.inline + input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_UP')) + if index !== items.length - 1 + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/down/${item.id}`) + .field.inline + .control.inline + input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_DOWN')) + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/bottom/${item.id}`) + .field.inline + .control.inline + input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_BOTTOM')) + if !archived + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/archive/${item.id}`) + .field.inline + .control.inline + input.inline.button.is-warning(type='submit' value=lang('WISHLIST_ARCHIVE')) + else + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/restore/${item.id}`) + .field.inline + .control.inline + input.inline.button.is-success(type='submit' value=lang('WISHLIST_RESTORE')) + block title h1 .level @@ -18,6 +252,9 @@ block title span= lang('WISHLIST_TITLE', name) .level-right .level-item + if ((!global._CC.config.pledgeSuggest && req.user.pledgeOnly) || !global._CC.config.suggestionsEnabled) + span= " " + else a(href="#addWishlistItem").button.button.is-primary.is-rounded span.icon i.fas.fa-plus @@ -37,155 +274,9 @@ block content +sharedInfoProp('coatSize', 'PROFILE_COAT_SIZE') +sharedInfoProp('hatSize', 'PROFILE_HAT_SIZE') +sharedInfoProp('phoneModel', 'PROFILE_PHONE_MODEL') - if global._CC.config.wishlist.table - .box - table.table.has-mobile-cards#wishlist-table - thead - th # - th(style='width: 15%;')= lang('WISHLIST_IMAGE') - th(style='width: 25%;')= lang('WISHLIST_NAME') - th(style='width: 50%;')= lang('WISHLIST_NOTE') - th(style='width: 10%; cursor: pointer') - a(href="#", id="sort-price")= lang('WISHLIST_PRICE') - |  - span#price-arrow - th= lang('WISHLIST_EDIT_ITEM') - th= lang('WISHLIST_ADDED_BY') - if req.params.user === req.user._id - th= lang('WISHLIST_MOVE_TOP') - th= lang('WISHLIST_MOVE_UP') - th= lang('WISHLIST_MOVE_DOWN') - th= lang('WISHLIST_MOVE_BOTTOM') - else - th= lang('WISHLIST_PLEDGE') - th= lang('WISHLIST_DELETE') - tbody.wishlist-items - each item, index in items - tr(id=item.id) - td.rank= index + 1 - td - figure(style='width: 100%; margin: 0;') - img(src=item.image, style='width: 100%; max-height: 20em; object-fit: contain;') - if item.url - td.ugc(data-label=lang('WISHLIST_NAME')) - if item.pledgedBy && item.addedBy != req.user._id - strike - a( - href=item.url, - rel='noopener noreferrer', - target='_blank' - )= (item.name ? item.name : item.url) - else - a( - href=item.url, - rel='noopener noreferrer', - target='_blank' - )= (item.name ? item.name : item.url) - else - td.ugc(data-label='Name') - if item.pledgedBy && item.addedBy != req.user._id - strike - span= item.name - else - span= item.name - if _CC.config.wishlist.note.markdown - td.ugc(data-label=lang('WISHLIST_NOTE')) - div!= compiledNotes[item.id] - else - td.ugc(data-label=lang('WISHLIST_NOTE'))= item.note - td.price.ugc(data-label=lang('WISHLIST_PRICE'))= item.price - td(data-label=lang('WISHLIST_EDIT_ITEM')) - form.inline(method='GET', action=`${_CC.config.base}wishlist/${req.params.user}/note/${item.id}`) - .field.inline - .control.inline - button.button.is-text( - type='submit', - style='text-decoration: none;' - disabled=item.addedBy !== req.user._id - ) - span.icon - i.far.fa-edit - if item.addedBy === '_CCUNKNOWN' - td.ugc(data-label=lang('WISHLIST_ADDED_BY'))= lang('WISHLIST_ADDED_BY_GUEST') - else - td.ugc(data-label=lang('WISHLIST_ADDED_BY'))= item.addedBy - if req.params.user === req.user._id - td(data-label=lang('WISHLIST_MOVE_ITEM_TOP')) - form.topForm.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/top/${item.id}`) - .field.inline - .control.inline - button.button.is-text( - type='submit', - style='text-decoration: none;', - disabled=index === 0 - ) - span.icon - i.fas.fa-angle-double-up - td(data-label=lang('WISHLIST_MOVE_ITEM_UP')) - form.upForm.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/up/${item.id}`) - .field.inline - .control.inline - button.button.is-text( - type='submit', - style='text-decoration: none;', - disabled=index === 0 - ) - span.icon - i.fas.fa-arrow-up - td(data-label=lang('WISHLIST_MOVE_ITEM_DOWN')) - form.downForm.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/down/${item.id}`) - .field.inline - .control.inline - button.button.is-text( - type='submit', - style='text-decoration: none;', - disabled=index === items.length - 1 - ) - span.icon - i.fas.fa-arrow-down - td(data-label=lang('WISHLIST_MOVE_ITEM_BOTTOM')) - form.bottomForm.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/bottom/${item.id}`) - .field.inline - .control.inline - button.button.is-text( - type='submit', - style='text-decoration: none;', - disabled=index === items.length - 1 - ) - span.icon - i.fas.fa-angle-double-down - else - td(data-label=lang('WISHLIST_PLEDGE')) - if req.params.user !== req.user._id && !item.pledgedBy - form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/pledge/${item.id}`) - .field.inline - .control.inline - input.inline.button.is-primary(type='submit' value=lang('WISHLIST_PLEDGE_ITEM')) - if item.pledgedBy === req.user._id - form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/unpledge/${item.id}`) - .field.inline - .control.inline - input.inline.button(type='submit' value=lang('WISHLIST_UNPLEDGE')) - if item.pledgedBy && item.pledgedBy !== req.user._id - if item.pledgedBy === '_CCUNKNOWN' - span.ugc=lang('WISHLIST_PLEDGED_GUEST') - else - span.ugc=lang('WISHLIST_PLEDGED', item.pledgedBy) - td(data-label='Delete Item') - form.inline( - method='POST', - action=`${_CC.config.base}wishlist/${req.params.user}/remove/${item.id}` - ) - .field.inline - .control.inline - button.button.is-text( - type='submit', - style='text-decoration: none;', - disabled=item.addedBy !== req.user._id - ) - span.icon - i.fas.fa-trash + if global._CC.config.wishlist.table + +wishlistTable(items.filter(item => !item.archived), false) else each item, index in items if req.user._id === item.addedBy || req.params.user !== req.user._id @@ -243,56 +334,76 @@ block content .field.inline .control.inline input.inline.button(type='submit' value=lang('WISHLIST_UNPLEDGE')) - if req.user._id === req.params.user - form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/remove/${item.id}`) + if req.user._id === req.params.user || (req.user.admin && _CC.config.adminCanEditAllWishlists) + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/remove/${item.id}`, onsubmit=`return confirm("${lang('WISHLIST_DELETE_CONFIRM')}")`) .field.inline .control.inline - input.inline.button.is-warning(type='submit' value=lang('WISHLIST_DELETE')) - if req.user._id === req.params.user || req.user._id === item.addedBy + input.inline.button.is-danger(type='submit' value=lang('WISHLIST_DELETE')) form.inline(method='GET', action=`${_CC.config.base}wishlist/${req.params.user}/note/${item.id}`) .field.inline .control.inline input.inline.button(type='submit', value=lang('WISHLIST_EDIT_ITEM')) - if index !== 0 && req.user._id === req.params.user - form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/up/${item.id}`) - .field.inline - .control.inline - input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_UP')) - if index !== items.length - 1 && req.user._id === req.params.user - form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/down/${item.id}`) + if !archived + if index !== 0 + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/up/${item.id}`) + .field.inline + .control.inline + input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_UP')) + if index !== items.length - 1 + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/down/${item.id}`) + .field.inline + .control.inline + input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_DOWN')) + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/bottom/${item.id}`) + .field.inline + .control.inline + input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_BOTTOM')) + if !archived + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/archive/${item.id}`) .field.inline .control.inline - input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_DOWN')) - if index !== items.length - 1 && req.user._id === req.params.user - form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/bottom/${item.id}`) + input.inline.button.is-warning(type='submit' value=lang('WISHLIST_ARCHIVE')) + else + form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/restore/${item.id}`) .field.inline .control.inline - input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_BOTTOM')) - form(method='POST')#addWishlistItem - .field - label.label!=lang('WISHLIST_URL_LABEL') - .control.has-icons-left - input.input( - type='text', - name='itemUrlOrName', - placeholder=lang('WISHLIST_URL_PLACEHOLDER') - ) - span.icon.is-small.is-left - i.fas.fa-gift - .field - label.label= lang('WISHLIST_NOTE') - .control - textarea.textarea( - name='note', - placeholder=lang('WISHLIST_OPTIONAL') - ) - .field.is-grouped - .control - input.button(type='submit' value=(req.user._id === req.params.user ? lang('WISHLIST_ADD') : lang('WISHLIST_PLEDGE_ITEM'))) - if req.user._id !== req.params.user + input.inline.button.is-success(type='submit' value=lang('WISHLIST_RESTORE')) + + if ((!global._CC.config.pledgeSuggest && req.user.pledgeOnly) || !global._CC.config.suggestionsEnabled) + .notification.is-info= lang('WISHLIST_SUGGESTIONS_DISABLED') + else + form(method='POST')#addWishlistItem + .field + label.label!=lang('WISHLIST_URL_LABEL') + .control.has-icons-left + input.input( + type='text', + name='itemUrlOrName', + placeholder=lang('WISHLIST_URL_PLACEHOLDER') + ) + span.icon.is-small.is-left + i.fas.fa-gift + .field + label.label= lang('WISHLIST_NOTE') + .control + textarea.textarea( + name='note', + placeholder=lang('WISHLIST_OPTIONAL') + ) + .field.is-grouped .control - input.inline.button(type='submit', value=lang('WISHLIST_SUGGEST'), name='suggest') + input.button(type='submit' value=(req.user._id === req.params.user ? lang('WISHLIST_ADD') : lang('WISHLIST_PLEDGE_ITEM'))) + if req.user._id !== req.params.user + .control + input.inline.button(type='submit', value=lang('WISHLIST_SUGGEST'), name='suggest') script(src=`${_CC.config.base}js/wishlist.js`) + br + details + summary= lang('WISHLIST_ARCHIVED_ITEMS') + if global._CC.config.wishlist.table + +wishlistTable(items.filter(item => item.archived), true) + else + +wishlistBox(items.filter(item => item.archived), true) block print h1 #{req.params.user}'s Wishlist diff --git a/src/views/wishlists.pug b/src/views/wishlists.pug index 45f39038..4f65dbd2 100644 --- a/src/views/wishlists.pug +++ b/src/views/wishlists.pug @@ -8,7 +8,7 @@ mixin wishlistDetails(wishlist, onlyShowAddedByUser) i.fas.fa-chevron-down ul.wishlist-dropdown for item in wishlist - if onlyShowAddedByUser === undefined || item.addedBy === onlyShowAddedByUser + if (!item.archived && onlyShowAddedByUser === undefined) || item.addedBy === onlyShowAddedByUser li if item.pledgedBy && !onlyShowAddedByUser strike= item.name @@ -52,7 +52,7 @@ block content div!= _CC.config.customHtml.wishlists ul.noStyle.noLeftMargin - if req.user._id !== '_CCUNKNOWN' + if req.user._id !== '_CCUNKNOWN' && !req.user.pledgeOnly li .box a(href=`${_CC.config.base}wishlist/${req.user._id}`, style='color: #4a4a4a;') @@ -67,7 +67,7 @@ block content progress.progress(value=1, max=1) +wishlistDetails(req.user.wishlist, req.user._id) each user in users - if req.user._id !== user.id + if req.user._id !== user.id && !user.doc.pledgeOnly li .box a(href=`${_CC.config.base}wishlist/${user.id}`, style='color: #4a4a4a;') @@ -77,8 +77,8 @@ block content figure.image.is-square.is-fullwidth.is-marginless(style='display: inline-block;') img.is-rounded.is-fullwidth(src=_CC.getUrlForPfp(user.doc.pfp), style='object-fit: cover;') .column - span= lang('WISHLISTS_COUNTS', user.id, totals(user.doc.wishlist).pledged, user.doc.wishlist.length) - progress.progress.is-info(value=totals(user.doc.wishlist).pledged, max=user.doc.wishlist.length) + span= lang('WISHLISTS_COUNTS', user.id, totals(user.doc.wishlist).pledged, totals(user.doc.wishlist).active) + progress.progress.is-info(value=totals(user.doc.wishlist).pledged, max=totals(user.doc.wishlist).active) +wishlistDetails(user.doc.wishlist) script. document.querySelectorAll('.wishlist-dropdown-button')