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')