Trent Larson
2 months ago
9 changed files with 390 additions and 32 deletions
@ -0,0 +1,118 @@ |
|||
<template> |
|||
<div v-if="visible" class="dialog-overlay"> |
|||
<div class="dialog"> |
|||
<h1 class="text-xl font-bold text-center mb-4">{{ title }}</h1> |
|||
|
|||
{{ message }} |
|||
<input |
|||
type="text" |
|||
placeholder="Name" |
|||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" |
|||
v-model="text" |
|||
/> |
|||
|
|||
<!-- Add date selection element --> |
|||
Expiration |
|||
<input |
|||
type="date" |
|||
class="block rounded border border-slate-400 mb-4 px-3 py-2" |
|||
v-model="expiresAt" |
|||
/> |
|||
|
|||
<div class="mt-8"> |
|||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> |
|||
<button |
|||
type="button" |
|||
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" |
|||
@click="onClickSaveChanges()" |
|||
> |
|||
Save |
|||
</button> |
|||
<!-- SHOW ME instead while processing saving changes --> |
|||
<button |
|||
type="button" |
|||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" |
|||
@click="onClickCancel()" |
|||
> |
|||
Cancel |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Vue, Component } from "vue-facing-decorator"; |
|||
|
|||
import { NotificationIface } from "@/constants/app"; |
|||
|
|||
@Component |
|||
export default class InviteDialog extends Vue { |
|||
$notify!: (notification: NotificationIface, timeout?: number) => void; |
|||
|
|||
callback: (text: string, expiresAt: string) => void = () => {}; |
|||
message = ""; |
|||
text = ""; |
|||
title = ""; |
|||
visible = false; |
|||
expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30 * 3) |
|||
.toISOString() |
|||
.substring(0, 10); |
|||
|
|||
async open( |
|||
title: string, |
|||
message: string, |
|||
aCallback: (text: string, expiresAt: string) => void, |
|||
) { |
|||
this.callback = aCallback; |
|||
this.title = title; |
|||
this.message = message; |
|||
this.visible = true; |
|||
} |
|||
|
|||
async onClickSaveChanges() { |
|||
if (!this.expiresAt) { |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "warning", |
|||
title: "Needs Expiration", |
|||
text: "You must select an expiration date.", |
|||
}, |
|||
5000, |
|||
); |
|||
} else { |
|||
this.callback(this.text, this.expiresAt); |
|||
this.visible = false; |
|||
} |
|||
} |
|||
|
|||
onClickCancel() { |
|||
this.visible = false; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.dialog-overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 1.5rem; |
|||
} |
|||
|
|||
.dialog { |
|||
background-color: white; |
|||
padding: 1rem; |
|||
border-radius: 0.5rem; |
|||
width: 100%; |
|||
max-width: 500px; |
|||
} |
|||
</style> |
@ -0,0 +1,195 @@ |
|||
<template> |
|||
<QuickNav selected="Invite" /> |
|||
<TopMessage /> |
|||
|
|||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> |
|||
<!-- Back --> |
|||
<div class="text-lg text-center font-light relative px-7"> |
|||
<h1 |
|||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" |
|||
@click="$router.back()" |
|||
> |
|||
<fa icon="chevron-left" class="fa-fw"></fa> |
|||
</h1> |
|||
</div> |
|||
|
|||
<!-- Heading --> |
|||
<h1 class="text-4xl text-center font-light">Invitations</h1> |
|||
|
|||
<!-- New Project --> |
|||
<button |
|||
v-if="isRegistered" |
|||
class="fixed right-6 top-12 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full" |
|||
@click="createInvite()" |
|||
> |
|||
<fa icon="plus" class="fa-fw"></fa> |
|||
</button> |
|||
|
|||
<InviteDialog ref="inviteDialog" /> |
|||
|
|||
<!-- Invites Table --> |
|||
<div v-if="invites.length" class="mt-6"> |
|||
<table class="min-w-full bg-white"> |
|||
<thead> |
|||
<tr> |
|||
<th class="py-2">ID</th> |
|||
<th class="py-2">Notes</th> |
|||
<th class="py-2">Expires At</th> |
|||
<th class="py-2">Redeemed By</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr |
|||
v-for="invite in invites" |
|||
:key="invite.inviteIdentifier" |
|||
class="border-t" |
|||
> |
|||
<td class="py-2 text-center"> |
|||
{{ getTruncatedInviteId(invite.inviteIdentifier) }} |
|||
</td> |
|||
<td class="py-2 text-left">{{ invite.notes }}</td> |
|||
<td class="py-2 text-center"> |
|||
{{ invite.expiresAt.substring(0, 10) }} |
|||
</td> |
|||
<td class="py-2 text-center"> |
|||
{{ getTruncatedRedeemedBy(invite.redeemedBy) }} |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
<p v-else class="mt-6 text-center">No invites found.</p> |
|||
</section> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { Component, Vue } from "vue-facing-decorator"; |
|||
import axios from "axios"; |
|||
|
|||
import { db, retrieveSettingsForActiveAccount } from "../db"; |
|||
import QuickNav from "@/components/QuickNav.vue"; |
|||
import TopMessage from "@/components/TopMessage.vue"; |
|||
import InviteDialog from "@/components/InviteDialog.vue"; |
|||
import { NotificationIface } from "@/constants/app"; |
|||
import { createInviteJwt, getHeaders } from "@/libs/endorserServer"; |
|||
|
|||
interface Invite { |
|||
inviteIdentifier: string; |
|||
expiresAt: string; |
|||
notes: string; |
|||
redeemedBy: string | null; |
|||
} |
|||
|
|||
@Component({ |
|||
components: { QuickNav, TopMessage, InviteDialog }, |
|||
}) |
|||
export default class InviteOneView extends Vue { |
|||
$notify!: (notification: NotificationIface, timeout?: number) => void; |
|||
|
|||
invites: Invite[] = []; |
|||
activeDid: string = ""; |
|||
apiServer: string = ""; |
|||
isRegistered: boolean = false; |
|||
|
|||
async mounted() { |
|||
try { |
|||
await db.open(); |
|||
const settings = await retrieveSettingsForActiveAccount(); |
|||
this.activeDid = settings.activeDid || ""; |
|||
this.apiServer = settings.apiServer || ""; |
|||
this.isRegistered = !!settings.isRegistered; |
|||
|
|||
const headers = await getHeaders(this.activeDid); |
|||
const response = await axios.get( |
|||
this.apiServer + "/api/userUtil/invite", |
|||
{ headers }, |
|||
); |
|||
this.invites = response.data.data; |
|||
} catch (error) { |
|||
console.error("Error fetching invites:", error); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Load Error", |
|||
text: "Got an error loading your invites.", |
|||
}, |
|||
5000, |
|||
); |
|||
} |
|||
} |
|||
|
|||
getTruncatedInviteId(inviteId: string): string { |
|||
if (inviteId.length <= 9) return inviteId; |
|||
return `${inviteId.slice(0, 3)}...${inviteId.slice(-3)}`; |
|||
} |
|||
|
|||
getTruncatedRedeemedBy(redeemedBy: string | null): string { |
|||
if (!redeemedBy) return "Not yet redeemed"; |
|||
if (redeemedBy.length <= 19) return redeemedBy; |
|||
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`; |
|||
} |
|||
|
|||
async createInvite() { |
|||
(this.$refs.inviteDialog as InviteDialog).open( |
|||
"Invitation Note", |
|||
`These notes are only for your use, to make comments for a link to recall later if redeemed by someone. |
|||
Note that this is sent to the server.`, |
|||
async (notes, expiresAt) => { |
|||
try { |
|||
const inviteIdentifier = |
|||
Math.random().toString(36).substring(2) + |
|||
Math.random().toString(36).substring(2) + |
|||
Math.random().toString(36).substring(2); |
|||
const headers = await getHeaders(this.activeDid); |
|||
if (!expiresAt) { |
|||
throw { |
|||
response: { |
|||
data: { error: "You must select an expiration date." }, |
|||
}, |
|||
}; |
|||
} |
|||
const expiresIn = |
|||
(new Date(expiresAt).getTime() - new Date().getTime()) / 1000; |
|||
const inviteJwt = await createInviteJwt( |
|||
this.activeDid, |
|||
undefined, |
|||
inviteIdentifier, |
|||
expiresIn, |
|||
); |
|||
await axios.post( |
|||
this.apiServer + "/api/userUtil/invite", |
|||
{ inviteJwt: inviteJwt, notes: notes }, |
|||
{ headers }, |
|||
); |
|||
this.invites.push({ |
|||
inviteIdentifier: inviteIdentifier, |
|||
expiresAt: expiresAt, |
|||
notes: notes, |
|||
redeemedBy: null, |
|||
}); |
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|||
} catch (error: any) { |
|||
console.error("Error creating invite:", error); |
|||
let message = "Got an error creating your invite."; |
|||
if ( |
|||
error.response && |
|||
error.response.data && |
|||
error.response.data.error |
|||
) { |
|||
message = error.response.data.error; |
|||
} |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error Creating Invite", |
|||
text: message, |
|||
}, |
|||
5000, |
|||
); |
|||
} |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
</script> |
Loading…
Reference in new issue