Added notification bell, WIP.
This commit is contained in:
		
							parent
							
								
									dc5affb4cf
								
							
						
					
					
						commit
						342ba6191e
					
				|  | @ -91,7 +91,7 @@ | ||||||
| 
 | 
 | ||||||
| 											<div class="result-label"> | 											<div class="result-label"> | ||||||
| 												{{ actor.name }} | 												{{ actor.name }} | ||||||
| 												<template v-if="actor.ageFromBirth || actor.origin?.country">({{ [actor.ageFromBirth, actor.origin?.country?.alpha2].join(', ') }})</template> | 												<template v-if="actor.ageFromBirth || actor.origin?.country">({{ [actor.ageFromBirth, actor.origin?.country?.alpha2].filter(Boolean).join(', ') }})</template> | ||||||
| 											</div> | 											</div> | ||||||
| 										</li> | 										</li> | ||||||
| 									</ul> | 									</ul> | ||||||
|  |  | ||||||
|  | @ -78,69 +78,92 @@ | ||||||
| 				</button> | 				</button> | ||||||
| 			</form> | 			</form> | ||||||
| 
 | 
 | ||||||
| 			<VDropdown | 			<div | ||||||
| 				v-if="user" | 				v-if="user" | ||||||
| 				:triggers="['click']" | 				class="userpanel" | ||||||
| 				:prevent-overflow="true" | 				:class="{ searching: searchFocused }" | ||||||
| 			> | 			> | ||||||
| 				<div | 				<VDropdown | ||||||
| 					class="userpanel" | 					:triggers="['click']" | ||||||
| 					:class="{ searching: searchFocused }" | 					:prevent-overflow="true" | ||||||
|  | 				> | ||||||
|  | 					<Icon | ||||||
|  | 						icon="bell2" | ||||||
|  | 						class="notifs-bell" | ||||||
|  | 						:class="{ unread: notifications.some((notif) => !notif.isSeen) }" | ||||||
|  | 					/> | ||||||
|  | 
 | ||||||
|  | 					<template #popper> | ||||||
|  | 						<div class="menu"> | ||||||
|  | 							<ul class="notifs nolist"> | ||||||
|  | 								<li | ||||||
|  | 									v-for="notif in notifications" | ||||||
|  | 									:key="notif.id" | ||||||
|  | 									class="notif" | ||||||
|  | 								>{{ notif.scene.title }}</li> | ||||||
|  | 							</ul> | ||||||
|  | 						</div> | ||||||
|  | 					</template> | ||||||
|  | 				</VDropdown> | ||||||
|  | 
 | ||||||
|  | 				<VDropdown | ||||||
|  | 					:triggers="['click']" | ||||||
|  | 					:prevent-overflow="true" | ||||||
| 				> | 				> | ||||||
| 					<img | 					<img | ||||||
| 						:src="user.avatar" | 						:src="user.avatar" | ||||||
| 						class="avatar" | 						class="avatar" | ||||||
| 					> | 					> | ||||||
| 				</div> |  | ||||||
| 
 | 
 | ||||||
| 				<template #popper> | 					<template #popper> | ||||||
| 					<div class="menu"> | 						<div class="menu"> | ||||||
| 						<a | 							<a | ||||||
| 							:href="`/user/${user.username}`" | 								:href="`/user/${user.username}`" | ||||||
| 							class="menu-header ellipsis" | 								class="menu-header ellipsis" | ||||||
| 						>{{ user.username }}</a> | 							>{{ user.username }}</a> | ||||||
| 
 | 
 | ||||||
| 						<ul class="menu-list nolist"> | 							<ul class="menu-list nolist"> | ||||||
| 							<li class="menu-item"> | 								<li class="menu-item"> | ||||||
| 								<a | 									<a | ||||||
| 									:href="`/user/${user.username}`" | 										:href="`/user/${user.username}`" | ||||||
| 									class="menu-button nolink" | 										class="menu-button nolink" | ||||||
|  | 									> | ||||||
|  | 										<Icon icon="user7" /> | ||||||
|  | 										Profile | ||||||
|  | 									</a> | ||||||
|  | 								</li> | ||||||
|  | 
 | ||||||
|  | 								<li | ||||||
|  | 									v-if="user.primaryStash" | ||||||
|  | 									class="menu-item" | ||||||
| 								> | 								> | ||||||
| 									<Icon icon="user7" /> | 									<a | ||||||
| 									Profile | 										:href="`/stash/${user.username}/${user.primaryStash.slug}`" | ||||||
| 								</a> | 										class="menu-button nolink favorites" | ||||||
| 							</li> | 									> | ||||||
|  | 										<Icon icon="heart7" /> | ||||||
|  | 										Favorites | ||||||
|  | 									</a> | ||||||
|  | 								</li> | ||||||
| 
 | 
 | ||||||
| 							<li | 								<li | ||||||
| 								v-if="user.primaryStash" | 									v-close-popper | ||||||
| 								class="menu-item" | 									class="menu-item menu-button" | ||||||
| 							> | 									@click="showSettings = true" | ||||||
| 								<a |  | ||||||
| 									:href="`/stash/${user.username}/${user.primaryStash.slug}`" |  | ||||||
| 									class="menu-button nolink favorites" |  | ||||||
| 								> | 								> | ||||||
| 									<Icon icon="heart7" /> | 									<Icon icon="equalizer" /> | ||||||
| 									Favorites | 									Settings | ||||||
| 								</a> | 								</li> | ||||||
| 							</li> |  | ||||||
| 
 | 
 | ||||||
| 							<li | 								<li | ||||||
| 								v-close-popper | 									class="menu-button menu-item logout" | ||||||
| 								class="menu-item menu-button" | 									@click="logout" | ||||||
| 								@click="showSettings = true" | 								><Icon icon="exit2" />Log out</li> | ||||||
| 							> | 							</ul> | ||||||
| 								<Icon icon="equalizer" /> | 						</div> | ||||||
| 								Settings | 					</template> | ||||||
| 							</li> | 				</VDropdown> | ||||||
| 
 | 			</div> | ||||||
| 							<li |  | ||||||
| 								class="menu-button menu-item logout" |  | ||||||
| 								@click="logout" |  | ||||||
| 							><Icon icon="exit2" />Log out</li> |  | ||||||
| 						</ul> |  | ||||||
| 					</div> |  | ||||||
| 				</template> |  | ||||||
| 			</VDropdown> |  | ||||||
| 
 | 
 | ||||||
| 			<div | 			<div | ||||||
| 				v-else-if="allowLogin" | 				v-else-if="allowLogin" | ||||||
|  | @ -181,11 +204,14 @@ import logo from '../../assets/img/logo.svg?raw'; // eslint-disable-line import/ | ||||||
| const pageContext = inject('pageContext'); | const pageContext = inject('pageContext'); | ||||||
| 
 | 
 | ||||||
| const user = pageContext.user; | const user = pageContext.user; | ||||||
|  | const notifications = pageContext.notifications; | ||||||
| const query = ref(pageContext.urlParsed.search.q || ''); | const query = ref(pageContext.urlParsed.search.q || ''); | ||||||
| const allowLogin = pageContext.env.allowLogin; | const allowLogin = pageContext.env.allowLogin; | ||||||
| const searchFocused = ref(false); | const searchFocused = ref(false); | ||||||
| const showSettings = ref(false); | const showSettings = ref(false); | ||||||
| 
 | 
 | ||||||
|  | console.log(notifications); | ||||||
|  | 
 | ||||||
| const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]); | const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]); | ||||||
| const currentPath = `${pageContext.urlParsed.pathnameOriginal}${pageContext.urlParsed.searchOriginal || ''}`; | const currentPath = `${pageContext.urlParsed.pathnameOriginal}${pageContext.urlParsed.searchOriginal || ''}`; | ||||||
| 
 | 
 | ||||||
|  | @ -316,7 +342,7 @@ function blurSearch(event) { | ||||||
| 	height: 100%; | 	height: 100%; | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 	align-items: center; | 	align-items: center; | ||||||
| 	padding: 0 1rem 0 1.5rem; | 	padding: 0 1rem 0 0; | ||||||
| 	font-size: 0; | 	font-size: 0; | ||||||
| 	cursor: pointer; | 	cursor: pointer; | ||||||
| 
 | 
 | ||||||
|  | @ -332,6 +358,31 @@ function blurSearch(event) { | ||||||
| 	object-fit: cover; | 	object-fit: cover; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .notifs-bell { | ||||||
|  | 	padding: 0 1.25rem; | ||||||
|  | 	height: 100%; | ||||||
|  | 	fill: var(--shadow); | ||||||
|  | 
 | ||||||
|  | 	&:hover, | ||||||
|  | 	&.unread { | ||||||
|  | 		cursor: pointer; | ||||||
|  | 		fill: var(--primary); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .notifs { | ||||||
|  | 	overflow: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .notif { | ||||||
|  | 	display: flex; | ||||||
|  | 	padding: .5rem 1rem; | ||||||
|  | 
 | ||||||
|  | 	&:not(:last-child) { | ||||||
|  | 		border-bottom: solid 1px var(--shadow-weak-30); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .login { | .login { | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 	align-items: center; | 	align-items: center; | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ export async function onBeforeRender(pageContext) { | ||||||
| 			: [], | 			: [], | ||||||
| 	]); | 	]); | ||||||
| 
 | 
 | ||||||
| 	console.log('out alerts', alerts); | 	console.log(pageContext.notifications); | ||||||
| 
 | 
 | ||||||
| 	if (!profile) { | 	if (!profile) { | ||||||
| 		throw render(404, `Cannot find user '${pageContext.routeParams.username}'.`); | 		throw render(404, `Cannot find user '${pageContext.routeParams.username}'.`); | ||||||
|  |  | ||||||
|  | @ -6,5 +6,6 @@ export default { | ||||||
| 		'urlParsed', | 		'urlParsed', | ||||||
| 		'env', | 		'env', | ||||||
| 		'user', | 		'user', | ||||||
|  | 		'notifications', | ||||||
| 	], | 	], | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -140,6 +140,28 @@ export async function removeAlert(alertId, reqUser) { | ||||||
| 		.delete(); | 		.delete(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export async function fetchNotifications(reqUser, options = {}) { | ||||||
|  | 	const notifications = await knex('notifications') | ||||||
|  | 		.select('notifications.*', 'alerts.id as alert_id', 'scenes.title as scene_title') | ||||||
|  | 		.leftJoin('releases as scenes', 'scenes.id', 'notifications.scene_id') | ||||||
|  | 		.leftJoin('alerts', 'alerts.id', 'notifications.alert_id') | ||||||
|  | 		.where('notifications.user_id', reqUser.id) | ||||||
|  | 		.limit(options.limit || 10) | ||||||
|  | 		.orderBy('created_at', 'desc'); | ||||||
|  | 
 | ||||||
|  | 	return notifications.map((notification) => ({ | ||||||
|  | 		id: notification.id, | ||||||
|  | 		sceneId: notification.scene_id, | ||||||
|  | 		scene: { | ||||||
|  | 			id: notification.scene_id, | ||||||
|  | 			title: notification.scene_title, | ||||||
|  | 		}, | ||||||
|  | 		alertId: notification.alert_id, | ||||||
|  | 		isSeen: notification.seen, | ||||||
|  | 		createdAt: notification.created_at, | ||||||
|  | 	})); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function updateNotification(notificationId, updatedNotification, reqUser) { | export async function updateNotification(notificationId, updatedNotification, reqUser) { | ||||||
| 	await knex('notifications') | 	await knex('notifications') | ||||||
| 		.where('id', notificationId) | 		.where('id', notificationId) | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import { | ||||||
| 	fetchAlerts, | 	fetchAlerts, | ||||||
| 	createAlert, | 	createAlert, | ||||||
| 	removeAlert, | 	removeAlert, | ||||||
|  | 	fetchNotifications, | ||||||
| 	updateNotifications, | 	updateNotifications, | ||||||
| 	updateNotification, | 	updateNotification, | ||||||
| } from '../alerts.js'; | } from '../alerts.js'; | ||||||
|  | @ -24,6 +25,14 @@ export async function removeAlertApi(req, res) { | ||||||
| 	res.status(204).send(); | 	res.status(204).send(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export async function fetchNotificationsApi(req, res) { | ||||||
|  | 	const notifications = await fetchNotifications(req.user, { | ||||||
|  | 		limit: req.query.limit || 10, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	res.send(notifications); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function updateNotificationsApi(req, res) { | export async function updateNotificationsApi(req, res) { | ||||||
| 	await updateNotifications(req.body, req.user); | 	await updateNotifications(req.body, req.user); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -51,6 +51,8 @@ import { | ||||||
| 	updateNotificationsApi, | 	updateNotificationsApi, | ||||||
| } from './alerts.js'; | } from './alerts.js'; | ||||||
| 
 | 
 | ||||||
|  | import { fetchNotifications } from '../alerts.js'; | ||||||
|  | 
 | ||||||
| import initLogger from '../logger.js'; | import initLogger from '../logger.js'; | ||||||
| 
 | 
 | ||||||
| const logger = initLogger(); | const logger = initLogger(); | ||||||
|  | @ -164,6 +166,8 @@ export default async function initServer() { | ||||||
| 	router.get('/api/tags', fetchTagsApi); | 	router.get('/api/tags', fetchTagsApi); | ||||||
| 
 | 
 | ||||||
| 	router.get('*', async (req, res, next) => { | 	router.get('*', async (req, res, next) => { | ||||||
|  | 		const notifications = await fetchNotifications(req.user, { limit: 20 }); | ||||||
|  | 
 | ||||||
| 		const pageContextInit = { | 		const pageContextInit = { | ||||||
| 			urlOriginal: req.originalUrl, | 			urlOriginal: req.originalUrl, | ||||||
| 			urlQuery: req.query, // vike's own query does not apply boolean parser
 | 			urlQuery: req.query, // vike's own query does not apply boolean parser
 | ||||||
|  | @ -185,6 +189,7 @@ export default async function initServer() { | ||||||
| 				maxAggregateSize: config.database.manticore.maxAggregateSize, | 				maxAggregateSize: config.database.manticore.maxAggregateSize, | ||||||
| 				media: config.media, | 				media: config.media, | ||||||
| 			}, | 			}, | ||||||
|  | 			notifications, | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		const pageContext = await renderPage(pageContextInit); | 		const pageContext = await renderPage(pageContextInit); | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue