Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(members): add new tickets order #108

Merged
merged 1 commit into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"naumovs.color-highlight",
Expand Down
3,038 changes: 2,041 additions & 997 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"vue-i18n": "^9.13.1",
"vue-number-animation": "^2.0.2",
"vue-router": "^4.5.0",
"vue-sonner": "^1.3.0",
"vue-tailwind-datepicker": "^1.7.3"
},
"devDependencies": {
Expand Down
36 changes: 34 additions & 2 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
</Head>
<LoadingSpinner v-if="state.isLoading" class="m-auto size-16" />
<router-view v-else />
<NotificationToast />
<Teleport to="body">
<Toaster
:position="width <= 600 ? 'top-center' : 'bottom-left'"
:toast-options="{
unstyled: true,
}"
:visible-toasts="5"></Toaster>
</Teleport>
</template>

<script lang="ts" setup>
Expand All @@ -14,10 +21,14 @@ import { useNotificationsStore } from './store/notifications';
import LoadingSpinner from '@/components/LoadingSpinner.vue';
import { useHead } from '@unhead/vue';
import { Head } from '@unhead/vue/components';
import { reactive } from 'vue';
import { useWindowSize } from '@vueuse/core';
import { isNil } from 'lodash';
import { markRaw, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { Toaster, toast } from 'vue-sonner';

const { width } = useWindowSize();
const i18n = useI18n();
const router = useRouter();
const notificationsStore = useNotificationsStore();
Expand Down Expand Up @@ -58,4 +69,25 @@ router.onError((error) => {
});
}
});

watch(
() => notificationsStore.history.length,
(_historyLength) => {
const history = notificationsStore.history;
const allNotificationsNotDismissed = history.filter(({ dismissed }) => isNil(dismissed));
const allNotificationsNotDismissedSorted = allNotificationsNotDismissed.sort(
(first, second) => new Date(second.created).getTime() - new Date(first.created).getTime(),
);
const [notification] = allNotificationsNotDismissedSorted;
if (notification) {
toast.custom(markRaw(NotificationToast), {
duration: Infinity,
componentProps: {
notification,
},
});
}
},
{ immediate: true, deep: true },
);
</script>
149 changes: 81 additions & 68 deletions src/components/NotificationToast.vue
Original file line number Diff line number Diff line change
@@ -1,75 +1,71 @@
<template>
<Teleport to="body">
<div
aria-live="assertive"
class="pointer-events-none fixed inset-0 z-[100] flex items-start px-4 py-6 sm:items-end sm:p-6">
<div class="flex w-full flex-col-reverse items-center gap-4 sm:items-start">
<!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
<TransitionGroup
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="max-sm:-translate-y-2 opacity-0 sm:-translate-x-2"
enter-to-class="max-sm:translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transform ease-out duration-150 transition"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="-translate-y-2 sm:translate-y-2 opacity-0">
<div
v-for="notification in notificationsStore.history.filter(({ dismissed }) => !dismissed)"
:key="`notification-${notification.id}`"
class="app-notification pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white p-4 shadow-lg ring-1 ring-black ring-opacity-[5%]"
:style="{
...(notification.timeout && {
['--notification-timeout']: `${notification.timeout}ms`,
}),
}"
:temporary="!!notification.timeout"
@animationend="() => onTimeoutAnimationEnd(notification)">
<div class="flex items-start">
<SvgIcon
aria-hidden="true"
:class="['size-6 shrink-0', getIconColorFromType(notification.type)]"
:path="notification.icon || getIconFromType(notification.type)"
type="mdi" />
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium text-gray-900">{{ getMessage(notification) }}</p>
<p
v-if="notification.description"
class="mt-1 whitespace-pre-line text-sm text-gray-500">
{{ notification.description }}
</p>
<div v-if="notification.actions?.length" class="-ml-2 mt-3 flex flex-row gap-6">
<AppButton
v-for="(action, index) in notification.actions"
:key="`notification-${notification.id}-action-${action.label}`"
:class="[
'whitespace-nowrap !px-2 !py-1 ',
index === 0
? 'text-indigo-600 hover:bg-indigo-50 hover:text-indigo-500 focus:ring-indigo-500'
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-500 focus:ring-gray-500',
]"
type="button"
@click="action.onClick">
{{ action.label }}
</AppButton>
</div>
</div>
<button
:id="`notification-${notification.id}-close`"
class="ml-4 inline-flex shrink-0 rounded-md bg-white p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
type="button"
@click="notificationsStore.dismissNotification(notification.id)">
<span class="sr-only">{{ $t('action.close') }}</span>
<SvgIcon aria-hidden="true" class="size-6" :path="mdiClose" type="mdi" />
</button>
</div>
</div>
</TransitionGroup>
<div
class="app-notification pointer-events-auto w-96 overflow-hidden rounded-lg bg-slate-800 p-4 shadow-lg max-sm:mx-auto dark:border dark:border-gray-700 dark:bg-gray-950"
:style="{
...(notification.timeout && {
['--notification-timeout']: `${notification.timeout}ms`,
}),
}"
:temporary="!!notification.timeout"
@animationend="() => onTimeoutAnimationEnd(notification)">
<div class="flex items-start">
<SvgIcon
aria-hidden="true"
:class="['mt-1 size-6 shrink-0', getIconColorFromType(notification.type)]"
:path="notification.icon || getIconFromType(notification.type)"
type="mdi" />
<div class="ml-3 w-0 flex-1 pt-1.5">
<p class="text-sm font-medium text-gray-100">
{{ getMessage(notification) }}
</p>
<p
v-if="notification.description"
class="mt-1 whitespace-pre-line pb-1.5 text-sm text-gray-400">
{{ notification.description }}
</p>
<div v-if="notification.actions?.length" class="-ml-2 flex flex-row gap-6">
<AppButtonText
v-for="(action, index) in notification.actions"
:key="`notification-${notification.id}-action-${action.label}`"
:class="[
'whitespace-nowrap !px-2 !py-1',
index === 0
? `text-sky-600 hover:bg-sky-950 hover:text-sky-500 focus:ring-sky-500
focus:ring-offset-sky-950`
: `text-gray-400 hover:bg-gray-700 hover:text-gray-300 focus:ring-gray-500
focus:ring-offset-sky-950`,
]"
color="none"
type="button"
@click="
() => {
emit('closeToast');
action.onClick?.();
}
">
{{ action.label }}
</AppButtonText>
</div>
</div>
<AppButtonIcon
:id="`notification-${notification.id}-close`"
class="ml-4 shrink-0 rounded-md text-gray-400 hover:bg-gray-400/30 hover:text-gray-300 active:bg-gray-400/40 active:text-gray-100"
:icon="mdiClose"
:title="$t('action.close')"
type="button"
@click="
() => {
notificationsStore.dismissNotification(notification.id);
emit('closeToast');
}
" />
</div>
</Teleport>
</div>
</template>

<script setup lang="ts">
import AppButton from './form/AppButton.vue';
import AppButtonIcon from './form/AppButtonIcon.vue';
import AppButtonText from './form/AppButtonText.vue';
import { AppErrorCode } from '@/helpers/errors';
import { AppNotification, StoreNotification, useNotificationsStore } from '@/store/notifications';
import {
Expand All @@ -80,11 +76,22 @@ import {
mdiHelpCircle,
mdiInformation,
} from '@mdi/js';
import { random } from 'lodash';
import { PropType } from 'vue';
import { useI18n } from 'vue-i18n';

const i18n = useI18n();
const notificationsStore = useNotificationsStore();

// https://github.com/xiaoluoboding/vue-sonner/issues/63#issuecomment-2060782447
const emit = defineEmits(['closeToast']);
defineProps({
notification: {
type: Object as PropType<StoreNotification>,
required: true,
},
});

const getIconFromType = (type: AppNotification['type']) => {
switch (type) {
case 'info':
Expand Down Expand Up @@ -118,6 +125,7 @@ const getIconColorFromType = (type: AppNotification['type']) => {
const onTimeoutAnimationEnd = (notification: StoreNotification) => {
if (!!notification.timeout && !notification.dismissed) {
notificationsStore.dismissNotification(notification.id);
emit('closeToast');
}
};

Expand All @@ -133,7 +141,12 @@ const getMessage = (notification: AppNotification) => {
case AppErrorCode.FORBIDDEN:
return i18n.t('errors.onForbidden.message');
default:
return i18n.t('errors.onUnknown.message');
const defaultErrorMessages = [
i18n.t('errors.onUnknown.message.corporate'),
i18n.t('errors.onUnknown.message.funny'),
i18n.t('errors.onUnknown.message.oops'),
];
return defaultErrorMessages[random(0, defaultErrorMessages.length - 1)];
}
}
};
Expand All @@ -149,7 +162,7 @@ const getMessage = (notification: AppNotification) => {
content: '';
display: block;
position: absolute;
bottom: 0;
top: 0;
left: 0;
right: 0;
height: 0.25rem;
Expand Down
1 change: 1 addition & 0 deletions src/components/audit/AuditEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const auditComponent = computed(() => {
case AuditAction.MEMBER_SUBSCRIPTION_UPDATE:
return AuditEntryMemberSubscription;
case AuditAction.MEMBER_TICKET_UPDATE:
case AuditAction.MEMBER_TICKET_ADD:
return AuditEntryMemberTicket;
default:
return AuditEntryInline;
Expand Down
5 changes: 3 additions & 2 deletions src/components/audit/AuditEntryInline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ import { AuditAction, AuditEvent } from '@/services/api/audit';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
import {
mdiCalendarEditOutline,
mdiCalendarStartOutline,
mdiCalendarRangeOutline,
mdiChevronDown,
mdiChevronUp,
mdiDevices,
Expand Down Expand Up @@ -187,7 +187,8 @@ const icon = computed(() => {
case AuditAction.MEMBER_ACTIVITY_UPDATE:
return mdiCalendarEditOutline;
case AuditAction.MEMBER_SUBSCRIPTION_UPDATE:
return mdiCalendarStartOutline;
return mdiCalendarRangeOutline;
case AuditAction.MEMBER_TICKET_ADD:
case AuditAction.MEMBER_TICKET_UPDATE:
return mdiTicket;
case AuditAction.KEYS_ACCESS:
Expand Down
26 changes: 26 additions & 0 deletions src/components/form/AppButtonIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template>
<button
class="flex items-center justify-center overflow-hidden rounded-full p-1 text-gray-400 outline-none transition-all hover:bg-gray-200 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 active:bg-gray-300 active:text-gray-900 dark:hover:bg-gray-200/5 dark:hover:text-gray-100 dark:focus:ring-offset-slate-900 dark:active:bg-gray-200/15 dark:active:text-white"
:disabled="loading">
<LoadingSpinner v-if="loading" class="max-h-full max-w-full grow" :stroke-width="4" />
<slot v-else>
<SvgIcon aria-hidden="true" class="size-full grow" :path="icon" type="mdi" />
<span v-if="$attrs.title" class="sr-only">{{ $attrs.title }}</span>
</slot>
</button>
</template>

<script setup lang="ts">
import LoadingSpinner from '../LoadingSpinner.vue';

defineProps({
loading: {
type: Boolean,
default: false,
},
icon: {
type: String,
required: true,
},
});
</script>
38 changes: 38 additions & 0 deletions src/components/form/AppButtonText.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<template>
<AppButton
:class="[
`border border-transparent px-4 py-2 font-medium outline-none transition-all
focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-slate-900`,
colors,
]">
<slot />
</AppButton>
</template>

<script setup lang="ts">
import AppButton from './AppButton.vue';
import { PropType, computed } from 'vue';

export type TextColor = 'sky' | 'amber' | 'gray' | 'none';

const props = defineProps({
color: {
type: String as PropType<TextColor>,
default: 'sky',
},
});

const colors = computed(() => {
switch (props.color) {
case 'none':
return '';
case 'sky':
return 'text-sky-600 hover:bg-sky-50 hover:text-sky-700 focus:ring-sky-500 dark:hover:bg-sky-950 dark:hover:text-sky-500';
case 'amber':
return 'text-amber-600 hover:bg-amber-50 hover:text-amber-700 focus:ring-amber-500 dark:hover:bg-amber-950 dark:hover:text-amber-500';
case 'gray':
default:
return 'text-gray-600 hover:bg-gray-50 hover:text-gray-700 focus:ring-gray-500 dark:hover:bg-gray-950 dark:hover:text-gray-500';
}
});
</script>
4 changes: 2 additions & 2 deletions src/components/layout/NavigationDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
item.active
? 'bg-amber-600 text-white'
: 'text-amber-100 hover:bg-amber-600 hover:text-white',
'group flex w-full flex-col items-center rounded-md p-3 text-xs font-medium no-underline transition-colors',
'group flex w-full flex-col items-center rounded-md p-3 text-xs font-medium no-underline transition-colors active:bg-amber-700',
]"
:to="item.to">
<SvgIcon
Expand All @@ -29,7 +29,7 @@
route.name === ROUTE_NAMES.USER.PROFILE
? 'bg-amber-600 text-white'
: 'text-amber-100 hover:bg-amber-600 hover:text-white',
'group mt-auto flex w-full flex-col items-center rounded-md p-3 text-xs font-medium no-underline',
'group mt-auto flex w-full flex-col items-center rounded-md p-3 text-xs font-medium no-underline transition-colors active:bg-amber-700',
]"
:to="{ name: ROUTE_NAMES.USER.PROFILE }">
<MembersThumbnail
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/en-GB/audit.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"message": "{author} updated the subscription for {member} to start on {started} instead of {previouslyStarted}",
"self": "{author} updated their subscription to start on {started} instead of {previouslyStarted}"
},
"MEMBER_TICKET_ADD": {
"message": "{author} added an order of {count} tickets to {member}",
"self": "{author} added an order {count} tickets to themselves"
},
"MEMBER_TICKET_UPDATE": {
"message": "{author} modified the number of tickets in an order, from previously {previousCount} to now {count} for {member}",
"self": "{author} modified the number of tickets in one of their orders, from previously {previousCount} to now {count}"
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/fr-FR/audit.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"message": "{author} a modifié l'abonnement de {member} pour débuter au {started} au lieu du {previouslyStarted}",
"self": "{author} a modifié son abonnement pour débuter au {started} au lieu du {previouslyStarted}"
},
"MEMBER_TICKET_ADD": {
"message": "{author} a ajouté une commande de {count} tickets pour {member}",
"self": "{author} s'est ajouté lui-même une commande de {count} tickets"
},
"MEMBER_TICKET_UPDATE": {
"message": "{author} a modifié le nombre de tickets d'une commande, de précédemment {previousCount} à désormais {count} pour {member}",
"self": "{author} a modifié le nombre de tickets d'une de ses commandes, de précédemment {previousCount} à désormais {count}"
Expand Down
Loading
Loading