Skip to content

Commit 1c58f76

Browse files
committed
implement auth + rsvp + attendee list
1 parent 497d21a commit 1c58f76

29 files changed

+2340
-19
lines changed

packages/frontendmu-astro/src/data/meetups-raw.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/frontendmu-nuxt/auth-utils/useAuth.ts

Lines changed: 568 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { computed, ref } from 'vue';
2+
export default function useAuthRedirect() {
3+
const duration = ref(2500);
4+
const countdown = ref(duration.value);
5+
const willRedirect = ref(false);
6+
const countDownPercentage = computed(() => {
7+
return (((duration.value - countdown.value) / duration.value) * 100) + '%';
8+
});
9+
10+
function setUrl() {
11+
let url = window.location.href;
12+
// Store in session storage
13+
sessionStorage.setItem('redirectUrl', url);
14+
}
15+
16+
function tryRedirect() {
17+
const redirectUrl = sessionStorage.getItem('redirectUrl');
18+
sessionStorage.removeItem('redirectUrl');
19+
if (redirectUrl) {
20+
willRedirect.value = true;
21+
// start the countdown
22+
setTimeout(() => {
23+
window.location.href = redirectUrl;
24+
} , duration.value);
25+
26+
let intervalDelay = 10;
27+
// start the countdown such that the progress bar is updated every 100ms and and the countdown reaches 0 in duration ms
28+
let interval = setInterval(() => {
29+
countdown.value -= intervalDelay;
30+
if (countdown.value <= 0) {
31+
clearInterval(interval);
32+
}
33+
}, intervalDelay);
34+
35+
36+
37+
return true;
38+
}
39+
}
40+
41+
return {
42+
setUrl,
43+
tryRedirect,
44+
countdown,
45+
duration,
46+
countDownPercentage,
47+
willRedirect
48+
}
49+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script setup lang="ts">
2+
import OrganiserQRCodeScanner from '@components/auth/OrganiserQRCodeScanner.vue';
3+
import useAuth, { getClient } from '../../auth-utils/useAuth';
4+
import { computed } from 'vue';
5+
import BaseButton from '@components/base/BaseButton.vue';
6+
7+
const { isLoggedIn, user } = useAuth(getClient());
8+
9+
const isAdmin = computed(() => user.value?.role === 'Admin')
10+
</script>
11+
12+
<template>
13+
<template v-if="isLoggedIn && isAdmin">
14+
<h2 class="text-center py-4 font-medium text-verse-300">
15+
Admins can Scan a QR Code to verify a user's attendance to a meetup.
16+
</h2>
17+
<Suspense>
18+
<OrganiserQRCodeScanner />
19+
</Suspense>
20+
</template>
21+
<div v-else class="flex justify-center text-verse-300">
22+
<div class="flex flex-col gap-4 ">
23+
<h2 class="text-center py-4 font-medium">
24+
Not authorized
25+
</h2>
26+
27+
<BaseButton>
28+
<a href="/">Back to homepage</a>
29+
</BaseButton>
30+
</div>
31+
</div>
32+
</template>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<script setup lang="ts">
2+
import { defineProps } from 'vue';
3+
4+
const props = defineProps<{
5+
meetupId: string
6+
userId: string
7+
}>();
8+
9+
// const coordinates = await getCoordinates();
10+
11+
const infoJsonString = JSON.stringify({
12+
meetupId: props.meetupId,
13+
userId: props.userId,
14+
// coordinates: coordinates
15+
});
16+
17+
18+
19+
async function getCoordinates() {
20+
if (import.meta.env.SSR) {
21+
return;
22+
}
23+
24+
const data: GeolocationPosition = await new Promise((resolve, reject) => {
25+
navigator.geolocation.getCurrentPosition(resolve, reject);
26+
});
27+
28+
if (!data) {
29+
throw new Error("AAAAH! No coordinates!")
30+
}
31+
32+
// * DON'T LEAK YOUR ADDRESS!
33+
return {
34+
"latitude": "76.12541",
35+
"longitude": "47.63588"
36+
}
37+
38+
// * actual code
39+
// return {
40+
// latitude: data.coords.latitude,
41+
// longitude: data.coords.longitude
42+
// };
43+
}
44+
45+
</script>
46+
47+
48+
<template>
49+
<div class=" bg-white rounded-lg shadow-lg p-4 z-[1000] relative w-full flex justify-center">
50+
<img class="w-full aspect-square object-contain max-w-4xl"
51+
:src="`https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${infoJsonString}`" alt="QR Code" />
52+
</div>
53+
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<template>
2+
<div class="contain text-white border border-red-500">
3+
<h1>Bran</h1>
4+
<p>It's dark!</p>
5+
6+
<pre v-if="isLoggedIn">
7+
{{ user }}
8+
</pre>
9+
<div v-else>
10+
<p>Not logged in</p>
11+
</div>
12+
13+
<div class="flex gap-2">
14+
<button v-if="isLoggedIn" class="p-2 bg-slate-500" @click.prevent="logout()">Logout</button>
15+
</div>
16+
</div>
17+
</template>
18+
19+
<script setup lang="ts">
20+
import useAuth, { getClient } from '../../auth-utils/useAuth';
21+
const { user, logout, isLoggedIn } = useAuth(getClient());
22+
</script>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script setup lang="ts">
2+
3+
const props = withDefaults(
4+
defineProps<{
5+
label: string
6+
value?: string
7+
disabled?: boolean
8+
labelClass?: string
9+
}>(),
10+
{
11+
disabled: false,
12+
labelClass: 'w-[100px]',
13+
})
14+
</script>
15+
16+
<template>
17+
<div class="flex flex-col md:flex-row gap-1 md:gap-4">
18+
<label :class="[labelClass]">{{ props.label }}</label>
19+
<input v-if="props.value" type="text" v-model="props.value" class="bg-transparent" :disabled="disabled" />
20+
21+
<slot />
22+
</div>
23+
</template>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<script setup lang="ts">
2+
import { RadioGroupLabel, RadioGroupOption } from "@headlessui/vue";
3+
import type { PropType } from "vue";
4+
5+
type option = {
6+
value: string;
7+
name: string;
8+
icon: any;
9+
};
10+
11+
defineProps<{
12+
options: option[],
13+
returnValueOnly?: boolean;
14+
disabled?: boolean;
15+
}>();
16+
</script>
17+
18+
<template>
19+
<RadioGroupOption as="template" v-for="option in options" :key="option.name" :disabled="disabled"
20+
:value="returnValueOnly ? option.value : option" v-slot="{ active, checked }">
21+
<div :class="[
22+
active ? '' : '',
23+
checked
24+
? 'transition-all duration-100 ring-slate-800 bg-white dark:ring-white dark:text-black ring-2 dark:hover:bg-slate-100'
25+
: 'dark:bg-slate-900 dark:ring-slate-700 ring-0 bg-white shadow-md dark:shadow-white/5 dark:ring-1 dark:ring-white/10 dark:text-white dark:hover:bg-slate-900',
26+
27+
'text-slate-700 flex items-center rounded-md py-1 text-sm font-semibold text-center px-2 relative cursor-pointer ',
28+
'md:h-auto',
29+
disabled && !checked ? 'opacity-30' : '',
30+
]">
31+
<RadioGroupLabel as="div" class="select-none text-center w-full">
32+
<Transition name="slide">
33+
<Icon name="material-symbols:check-circle" v-if="checked"
34+
class="bg-slate-800 dark:bg-white absolute -right-3 top-1/2 -translate-y-1/2 rounded-full w-5 h-5 text-green-600" />
35+
</Transition>
36+
37+
<div class="w-full text-center">
38+
{{ option.name }}
39+
</div>
40+
</RadioGroupLabel>
41+
</div>
42+
</RadioGroupOption>
43+
</template>
44+
45+
<style>
46+
.slide-enter-active {
47+
transition: all 0.2s ease-out;
48+
}
49+
50+
.slide-leave-active {
51+
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
52+
}
53+
54+
.slide-enter-from {
55+
transform: translateX(5px);
56+
opacity: 0;
57+
}
58+
59+
.slide-leave-to {
60+
transform: translateX(-5px);
61+
opacity: 0;
62+
}
63+
</style>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<div class="contain text-white border border-red-500">
3+
<h1>Hodor</h1>
4+
<p>Hold the door!</p>
5+
6+
<div class="flex gap-2">
7+
<button v-if="isLoggedIn" class="p-2 bg-slate-500" @click.prevent="logout()">Logout</button>
8+
</div>
9+
10+
<div>
11+
<pre>{{ responseFromServer }}</pre>
12+
</div>
13+
14+
<LoginForm />
15+
</div>
16+
</template>
17+
18+
<script setup lang="ts">
19+
import { onMounted } from 'vue';
20+
import useAuth, { getClient } from '../../auth-utils/useAuth';
21+
import { DIRECTUS_URL } from '@utils/helpers';
22+
import LoginForm from './LoginForm.vue';
23+
const { user, logout, isLoggedIn, getCurrentUser, responseFromServer, checkIfLoggedIn } = useAuth(getClient());
24+
25+
function inlineLogin() {
26+
const currentPage = new URL(window.location.href);
27+
window.location.href = `${DIRECTUS_URL()}/auth/login/google?redirect=${currentPage}redirect`
28+
}
29+
30+
onMounted(() => {
31+
checkIfLoggedIn();
32+
})
33+
</script>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script setup lang="ts">
2+
import { onMounted } from 'vue';
3+
import useAuth, { getClient } from '../../auth-utils/useAuth';
4+
const { user, logout, isLoggedIn, getCurrentUser, responseFromServer, checkIfLoggedIn, avatarUrl } = useAuth(getClient());
5+
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
6+
import { ChevronDownIcon } from '@heroicons/vue/20/solid'
7+
import useAuthRedirect from '@/auth-utils/useAuthRedirect';
8+
9+
const { setUrl } = useAuthRedirect()
10+
11+
onMounted(() => {
12+
checkIfLoggedIn();
13+
});
14+
15+
</script>
16+
17+
<template>
18+
<div class="dark:text-zinc-200 dark:ring-white/10 pl-4">
19+
20+
<BaseButton v-if="!isLoggedIn" href="/login" :color="'primary'" class="font-bold" @click="setUrl()">
21+
Log In
22+
</BaseButton>
23+
<div v-else class="flex gap-2 items-center">
24+
<Menu as="div" class="relative inline-block text-left">
25+
<div>
26+
<MenuButton
27+
class="inline-flex items-center w-full justify-center gap-x-1.5 rounded-full text-sm font-semibold text-verse-900 dark:text-verse-100 shadow-sm ring-gray-300 hover:bg-gray-50">
28+
<div v-if="avatarUrl">
29+
<img class="w-10 aspect-square rounded-full" :src="avatarUrl">
30+
</div>
31+
<!-- <ChevronDownIcon class="-mr-1 h-5 w-5 text-gray-400" aria-hidden="true" /> -->
32+
</MenuButton>
33+
</div>
34+
35+
<transition enter-active-class="transition ease-out duration-100"
36+
enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100"
37+
leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100"
38+
leave-to-class="transform opacity-0 scale-95">
39+
<MenuItems
40+
class="absolute right-0 z-10 mt-2 w-56 origin-top-right divide-y divide-gray-100/10 rounded-md bg-zinc-500/20 dark:bg-verse-500/20 backdrop-blur-2xl shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
41+
<div class="py-1">
42+
<MenuItem v-slot="{ active }">
43+
<a href="/user/me"
44+
:class="[active ? 'bg-gray-400/10 text-verse-900 dark:text-verse-100' : 'text-verse-900 dark:text-verse-100', 'block px-4 py-2 text-sm']">My
45+
Profile</a>
46+
</MenuItem>
47+
<MenuItem v-slot="{ active }">
48+
<a @click="logout()"
49+
:class="[active ? 'bg-gray-400/10 text-verse-900 dark:text-verse-100' : 'text-verse-900 dark:text-verse-100', 'block px-4 py-2 text-sm cursor-pointer']">Logout</a>
50+
</MenuItem>
51+
</div>
52+
<div class="text-verse-900 dark:text-verse-100 p-4 text-sm">
53+
<div class="flex flex-col items-center justify-center gap-2">
54+
<div v-if="avatarUrl">
55+
<img class="w-16 aspect-square rounded-full" :src="avatarUrl">
56+
</div>
57+
58+
{{ user?.full_name }}
59+
</div>
60+
</div>
61+
62+
</MenuItems>
63+
</transition>
64+
</Menu>
65+
66+
67+
</div>
68+
</div>
69+
</template>

0 commit comments

Comments
 (0)