Skip to content
Open
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
2 changes: 1 addition & 1 deletion app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const pageName = computed<string>(() => !route.name || typeof route.name !== 'st
/>

<ClientOnly>
<CookieBanner />
<ConsentBanner />
<UiToaster />
<ModalGroup />
<ConfirmDialog
Expand Down
138 changes: 138 additions & 0 deletions app/components/atoms/ConsentBanner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<script setup lang="ts">
const {
showPopup,
consents,
consentTypes,
toggleConsent,
acceptAll,
rejectAll,
savePreferences,
} = useConsent()

const showSettings = ref(false)
</script>

<template>
<div
v-if="showPopup"
tabindex="0"
:aria-label="$t('components.ConsentBanner.banner.title')"
aria-modal="true"
role="dialog"
:data-state="showPopup ? 'open' : 'closed'"
data-test-banner
class="fixed border-4 border-muted bg-background shadow-lg z-50 md:w-md overflow-hidden rounded-2xl right-4 left-4 bottom-4 p-4 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
>
<div
v-if="!showSettings"
data-test-banner-info
class="flex flex-col gap-6"
>
<div class="flex flex-col gap-1">
<p class="font-bold">
{{ $t('components.ConsentBanner.banner.title') }}
</p>
<p class="text-muted-foreground text-sm">
{{ $t('components.ConsentBanner.banner.text') }}
</p>
</div>
<div class="flex flex-col md:flex-row justify-between gap-2">
<div class="flex flex-row gap-2">
<UiButton
data-test-reject-all
variant="secondary"
class="w-full md:w-fit"
@click="rejectAll"
>
{{ $t('components.ConsentBanner.rejectAll') }}
</UiButton>
<UiButton
data-test-accept-all
variant="secondary"
class="w-full md:w-fit"
@click="acceptAll"
>
{{ $t('components.ConsentBanner.acceptAll') }}
</UiButton>
</div>
<UiButton
data-test-customize
variant="tertiary"
@click="showSettings = true"
>
{{ $t('components.ConsentBanner.customize') }}
</UiButton>
</div>
</div>

<div
v-else
data-test-banner-settings
class="flex flex-col gap-6"
>
<div class="flex flex-col gap-1">
<p class="font-bold">
{{ $t('components.ConsentBanner.settings.title') }}
</p>
<p class="text-muted-foreground text-sm">
{{ $t('components.ConsentBanner.settings.text') }}
</p>
</div>

<div class="flex flex-col gap-2">
<div
v-for="type in consentTypes"
:key="type"
class="border rounded-lg p-4 flex flex-col gap-2"
>
<div class="flex items-center justify-between">
<UiLabel :for="type">
{{ $t(`components.ConsentBanner.${type}`) }}
</UiLabel>
<UiSwitch
:id="type"
:checked="consents?.[type] || false"
:default-value="consents?.[type] || false"
:disabled="type === 'necessary'"
@update:model-value="() => {
console.log('toggling consent for', type)
toggleConsent(type)
}"
/>
</div>
<p class="text-sm text-muted-foreground">
{{ $t(`components.ConsentBanner.${type}Text`) }}
</p>
</div>
</div>

<div class="flex flex-col md:flex-row justify-between gap-2">
<div class="flex flex-row gap-2">
<UiButton
data-test-reject-all
class="w-full md:w-fit"
variant="secondary"
@click="rejectAll"
>
{{ $t('components.ConsentBanner.rejectAll') }}
</UiButton>
<UiButton
data-test-accept-all
class="w-full md:w-fit"
variant="secondary"
@click="acceptAll"
>
{{ $t('components.ConsentBanner.acceptAll') }}
</UiButton>
</div>
<UiButton
data-test-save-preferences
variant="tertiary"
@click="savePreferences"
>
{{ $t('components.ConsentBanner.saveSettings') }}
</UiButton>
</div>
</div>
</div>
</template>
42 changes: 0 additions & 42 deletions app/components/atoms/CookieBanner.vue

This file was deleted.

73 changes: 73 additions & 0 deletions app/composables/useConsent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { configureConsentManager, createConsentManagerStore } from 'c15t'
import type { AllConsentNames } from 'c15t'

let consentManager: ReturnType<typeof configureConsentManager>
let c15tStore: ReturnType<typeof createConsentManagerStore>

export function useConsent() {
const { c15tUrl } = useRuntimeConfig().public

if (!consentManager) {
consentManager = configureConsentManager({
mode: 'c15t',
backendURL: c15tUrl,
})
}

if (!c15tStore) {
c15tStore = createConsentManagerStore(consentManager, {
initialGdprTypes: ['necessary', 'measurement'],
})
}

const consents = ref(c15tStore.getState().consents)
const consentTypes = c15tStore.getState().gdprTypes
const showPopup = ref(c15tStore?.getState().showPopup ?? false)

onMounted(() => c15tStore?.subscribe(updateState))

function updateState() {
showPopup.value = c15tStore?.getState().showPopup ?? false
consents.value = c15tStore?.getState().consents
}

function toggleConsent(type: AllConsentNames) {
if (type !== 'necessary') {
consents.value[type] = !consents.value[type]
}
}

function acceptAll() {
consentTypes.forEach(type => setConsent(type, true))
showPopup.value = false
}

function rejectAll() {
consentTypes.forEach((type) => {
if (type !== 'necessary') setConsent(type, false)
})
showPopup.value = false
}

function savePreferences() {
Object.entries(consents.value ?? {}).forEach(([type, value]) => {
setConsent(type as AllConsentNames, value)
})
showPopup.value = false
}

function setConsent(type: AllConsentNames, value: boolean) {
c15tStore?.getState().setConsent(type, value)
}

return {
showPopup,
consents,
consentTypes,
setConsent,
toggleConsent,
acceptAll,
rejectAll,
savePreferences,
}
}
2 changes: 1 addition & 1 deletion app/error.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defineProps({
<div class="flex flex-col items-center min-h-screen">
<div class="flex flex-col gap-y-6 items-center max-w-prose px-8 text-center pt-10">
<div class="text-[100px] font-black">
{{ error!.statusCode }}
{{ error!.status }}
</div>
<h1 class="text-primary">
{{ $t('pages.error.404') }}
Expand Down
37 changes: 34 additions & 3 deletions app/plugins/vue-query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
import { VueQueryPlugin } from '@tanstack/vue-query'
import type {
DehydratedState,
VueQueryPluginOptions,
} from '@tanstack/vue-query'
import {
VueQueryPlugin,
QueryClient,
hydrate,
dehydrate,
} from '@tanstack/vue-query'

export default defineNuxtPlugin({
name: 'vue-query',
parallel: true,
setup(nuxtApp) {
nuxtApp.vueApp.use(VueQueryPlugin)
setup(nuxt) {
const vueQueryState = useState<DehydratedState | null>('vue-query')

const queryClient = new QueryClient({
defaultOptions: {
queries: {},
},
})

const options: VueQueryPluginOptions = { queryClient }

nuxt.vueApp.use(VueQueryPlugin, options)

if (import.meta.server) {
nuxt.hooks.hook('app:rendered', () => {
vueQueryState.value = dehydrate(queryClient)
})
}

if (import.meta.client) {
nuxt.hooks.hook('app:created', () => {
hydrate(queryClient, vueQueryState.value)
})
}
},
})
4 changes: 2 additions & 2 deletions app/utils/ui-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,15 @@ export function validateParamId(id: string | string[] | undefined): number {
!id
|| typeof id !== 'string'
|| isNaN(+id)
) throw createError({ statusCode: 404, statusMessage: 'Id is not valid' })
) throw createError({ status: 404, statusText: 'Id is not valid' })

return +id
}

export function validateInject<T>(key: InjectionKey<T>): T {
const injection = inject(key)

if (!injection) throw createError({ statusCode: 500, statusMessage: 'Injection not found' })
if (!injection) throw createError({ status: 500, statusText: 'Injection not found' })

return injection
}
Expand Down
20 changes: 17 additions & 3 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -626,10 +626,24 @@
"options": "Toggle avatar options",
"deprecated": "You have a old avatar, please update it."
},
"cookieBanner": {
"text": "Embark on an Epic Adventure: Accept Cookies to Enhance Your D&D Experience!",
"ConsentBanner": {
"rejectAll": "Reject all",
"acceptAll": "Accept all",
"customize": "Customize",
"saveSettings": "Save settings",
"policy": "Cookie policy",
"button": "OK"
"necessary": "Necessary",
"necessaryText": "These cookies are essential for the website to function properly. They ensure basic functionalities and security features of the website, anonymously.",
"measurement": "Analytics",
"measurementText": "These cookies help us understand how visitors interact with our website by collecting and reporting information anonymously.",
"banner": {
"title": "We value your privacy",
"text": "This site uses cookies to improve your browsing experience, analyze site traffic."
},
"settings": {
"title": "Privacy Settings",
"text": "Customize your privacy settings here. You can choose which types of cookies and tracking technologies you allow."
}
},
"hero": {
"title": "Run immersive D&D campaigns your way",
Expand Down
22 changes: 18 additions & 4 deletions i18n/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -626,10 +626,24 @@
"options": "In/uitschakelen avatar opties",
"deprecated": "Je hebt een oude avatar, kies een nieuwe"
},
"cookieBanner": {
"text": "Start een Episch Avontuur: Accepteer Cookies voor Verbetering van je D&D Ervaring!",
"policy": "Cookie policy",
"button": "Oke"
"ConsentBanner": {
"rejectAll": "Weiger alle",
"acceptAll": "Accepteer alle",
"customize": "Aanpassen",
"saveSettings": "Instellingen opslaan",
"policy": "Cookiebeleid",
"necessary": "Noodzakelijk",
"necessaryText": "Deze cookies zijn essentieel voor de goede werking van de website. Ze zorgen voor basisfunctionaliteiten en beveiligingsfuncties van de website, anoniem.",
"measurement": "Analyse",
"measurementText": "Deze cookies helpen ons te begrijpen hoe bezoekers omgaan met de website door anonieme gegevens te verzamelen en te rapporteren.",
"banner": {
"title": "Wij hechten veel waarde aan uw privacy",
"text": "Deze website maakt gebruik van cookies om uw browse-ervaring te verbeteren en het websiteverkeer te analyseren."
},
"settings": {
"title": "Privacy instellingen",
"text": "Pas hier je privacy-instellingen aan. Je kunt kiezen welke soorten cookies en trackingtechnologieën je toestaat."
}
},
"hero": {
"title": "Voer meeslepende D&D campagnes op jouw manier",
Expand Down
Loading