prettier fixes #16

This commit is contained in:
Michael Dausmann
2023-10-24 21:18:03 +11:00
parent dc9d64ebf5
commit a7f8c37f99
56 changed files with 1706 additions and 935 deletions

View File

@@ -3,10 +3,10 @@
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
const accountStore = useAccountStore();
const { activeMembership, activeAccountMembers } = storeToRefs(accountStore)
const { activeMembership, activeAccountMembers } = storeToRefs(accountStore);
const config = useRuntimeConfig();
const newAccountName = ref("");
const newAccountName = ref('');
onMounted(async () => {
await accountStore.init();
@@ -14,8 +14,12 @@
});
function formatDate(date: Date | undefined) {
if (!date) { return ""; }
return new Intl.DateTimeFormat('default', { dateStyle: 'long' }).format(date);
if (!date) {
return '';
}
return new Intl.DateTimeFormat('default', { dateStyle: 'long' }).format(
date
);
}
function joinURL() {
@@ -25,7 +29,9 @@
<template>
<div class="container mx-auto p-6">
<div class="text-center mb-12">
<h2 class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl md:text-6xl mb-4">Account Information
<h2
class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl md:text-6xl mb-4">
Account Information
</h2>
</div>
@@ -34,24 +40,39 @@
<span class="font-bold w-32">Account Name:</span>
<span>{{ activeMembership?.account.name }}</span>
<template
v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN)">
<input v-model="newAccountName" type="text" class="p-2 border border-gray-400 rounded w-1/3"
placeholder="Enter new name">
<button @click.prevent="accountStore.changeAccountName(newAccountName)"
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">Change Name</button>
v-if="
activeMembership &&
(activeMembership.access === ACCOUNT_ACCESS.OWNER ||
activeMembership.access === ACCOUNT_ACCESS.ADMIN)
">
<input
v-model="newAccountName"
type="text"
class="p-2 border border-gray-400 rounded w-1/3"
placeholder="Enter new name" />
<button
@click.prevent="accountStore.changeAccountName(newAccountName)"
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
Change Name
</button>
</template>
</div>
<div class="flex gap-4 items-center">
<span class="font-bold w-32">Current Period Ends:</span>
<span>{{ formatDate(activeMembership?.account.current_period_ends) }}</span>
<span>{{
formatDate(activeMembership?.account.current_period_ends)
}}</span>
</div>
<div class="flex gap-4 items-center">
<span class="font-bold w-32">Permitted Features:</span>
<div class="flex flex-wrap gap-2">
<span v-for="feature in activeMembership?.account.features"
class="bg-gray-200 text-gray-700 font-semibold py-1 px-2 rounded-full">{{ feature }}</span>
<span
v-for="feature in activeMembership?.account.features"
class="bg-gray-200 text-gray-700 font-semibold py-1 px-2 rounded-full">
{{ feature }}
</span>
</div>
</div>
@@ -62,7 +83,10 @@
<div class="flex gap-4 items-center">
<span class="font-bold w-32">AI Gens for this Month/Max:</span>
<span>{{ activeMembership?.account.ai_gen_count }} / {{ activeMembership?.account.ai_gen_max_pm }}</span>
<span>
{{ activeMembership?.account.ai_gen_count }} /
{{ activeMembership?.account.ai_gen_max_pm }}
</span>
</div>
<div class="flex gap-4 items-center">
@@ -72,9 +96,15 @@
<div class="flex gap-4 items-center">
<span class="font-bold w-32">Access Level:</span>
<span class="bg-green-500 text-white font-semibold py-1 px-2 rounded-full">{{ activeMembership?.access }}</span>
<button @click.prevent="accountStore.claimOwnershipOfAccount()"
v-if="activeMembership && activeMembership.access === ACCOUNT_ACCESS.ADMIN "
<span
class="bg-green-500 text-white font-semibold py-1 px-2 rounded-full">
{{ activeMembership?.access }}
</span>
<button
@click.prevent="accountStore.claimOwnershipOfAccount()"
v-if="
activeMembership && activeMembership.access === ACCOUNT_ACCESS.ADMIN
"
class="bg-orange-500 hover:bg-orange-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Claim Ownership
</button>
@@ -88,47 +118,109 @@
<div class="flex gap-4 items-center">
<span class="font-bold w-32">Join Link:</span>
<div class="flex gap-2 items-center">
<input disabled type="text" class="p-2 border border-gray-400 rounded w-full" :value="joinURL()">
<button @click.prevent="accountStore.rotateJoinPassword()"
v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN)"
class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">ReGen</button>
<input
disabled
type="text"
class="p-2 border border-gray-400 rounded w-full"
:value="joinURL()" />
<button
@click.prevent="accountStore.rotateJoinPassword()"
v-if="
activeMembership &&
(activeMembership.access === ACCOUNT_ACCESS.OWNER ||
activeMembership.access === ACCOUNT_ACCESS.ADMIN)
"
class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
ReGen
</button>
</div>
</div>
<div class="flex flex-col gap-4">
<h2 class="text-lg font-bold">Members</h2>
<div class="flex flex-col gap-2">
<div v-for="accountMember in activeAccountMembers" class="flex gap-4 items-center">
<div
v-for="accountMember in activeAccountMembers"
class="flex gap-4 items-center">
<span>{{ accountMember.user.display_name }}</span>
<span class="bg-green-500 text-white font-semibold py-1 px-2 rounded-full">{{ accountMember.access }}</span>
<span
class="bg-green-500 text-white font-semibold py-1 px-2 rounded-full">
{{ accountMember.access }}
</span>
<span class="text-gray-500">({{ accountMember.user.email }})</span>
<button @click.prevent="accountStore.acceptPendingMembership(accountMember.id)"
v-if="accountMember.pending && activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN)"
<button
@click.prevent="
accountStore.acceptPendingMembership(accountMember.id)
"
v-if="
accountMember.pending &&
activeMembership &&
(activeMembership.access === ACCOUNT_ACCESS.OWNER ||
activeMembership.access === ACCOUNT_ACCESS.ADMIN)
"
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Accept Pending Membership
</button>
<button @click.prevent="accountStore.rejectPendingMembership(accountMember.id)"
v-if="accountMember.pending && activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN)"
<button
@click.prevent="
accountStore.rejectPendingMembership(accountMember.id)
"
v-if="
accountMember.pending &&
activeMembership &&
(activeMembership.access === ACCOUNT_ACCESS.OWNER ||
activeMembership.access === ACCOUNT_ACCESS.ADMIN)
"
class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Reject Pending Membership
</button>
<button @click.prevent="accountStore.changeUserAccessWithinAccount(accountMember.user.id, ACCOUNT_ACCESS.READ_WRITE)"
v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN) && accountMember.access === ACCOUNT_ACCESS.READ_ONLY && !accountMember.pending"
<button
@click.prevent="
accountStore.changeUserAccessWithinAccount(
accountMember.user.id,
ACCOUNT_ACCESS.READ_WRITE
)
"
v-if="
activeMembership &&
(activeMembership.access === ACCOUNT_ACCESS.OWNER ||
activeMembership.access === ACCOUNT_ACCESS.ADMIN) &&
accountMember.access === ACCOUNT_ACCESS.READ_ONLY &&
!accountMember.pending
"
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Promote to Read/Write
</button>
<button @click.prevent="accountStore.changeUserAccessWithinAccount(accountMember.user.id, ACCOUNT_ACCESS.ADMIN)"
v-if="activeMembership && activeMembership.access === ACCOUNT_ACCESS.OWNER && accountMember.access === ACCOUNT_ACCESS.READ_WRITE && !accountMember.pending"
<button
@click.prevent="
accountStore.changeUserAccessWithinAccount(
accountMember.user.id,
ACCOUNT_ACCESS.ADMIN
)
"
v-if="
activeMembership &&
activeMembership.access === ACCOUNT_ACCESS.OWNER &&
accountMember.access === ACCOUNT_ACCESS.READ_WRITE &&
!accountMember.pending
"
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Promote to Admin
</button>
<button @click.prevent="accountStore.deleteMembership(accountMember.id)"
v-if="activeMembership && activeMembership.access === ACCOUNT_ACCESS.OWNER && accountMember.access !== ACCOUNT_ACCESS.OWNER && !accountMember.pending"
<button
@click.prevent="accountStore.deleteMembership(accountMember.id)"
v-if="
activeMembership &&
activeMembership.access === ACCOUNT_ACCESS.OWNER &&
accountMember.access !== ACCOUNT_ACCESS.OWNER &&
!accountMember.pending
"
class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Remove
</button>
</div>
</div>
</div>
</div>
</div>
</div></template>
</template>

View File

@@ -1,8 +1,6 @@
<template>
<div class="prose lg:prose-xl m-5">
<p>
We are sorry that you canceled your transaction!
</p>
<p>We are sorry that you canceled your transaction!</p>
<p>
<NuxtLink to="/pricing">Pricing</NuxtLink>
</p>
@@ -10,4 +8,4 @@
<NuxtLink to="/dashboard">To Your Dashboard</NuxtLink>
</p>
</div>
</template>
</template>

View File

@@ -3,56 +3,86 @@
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
definePageMeta({
middleware: ['auth'],
middleware: ['auth']
});
const accountStore = useAccountStore();
const { activeMembership } = storeToRefs(accountStore)
const { activeMembership } = storeToRefs(accountStore);
const notesStore = useNotesStore();
const { notes } = storeToRefs(notesStore); // ensure the notes list is reactive
const newNoteText = ref('')
const { notes } = storeToRefs(notesStore); // ensure the notes list is reactive
const newNoteText = ref('');
async function addNote(){
await notesStore.createNote(newNoteText.value)
async function addNote() {
await notesStore.createNote(newNoteText.value);
newNoteText.value = '';
}
async function genNote(){
const genNoteText = await notesStore.generateAINoteFromPrompt(newNoteText.value)
async function genNote() {
const genNoteText = await notesStore.generateAINoteFromPrompt(
newNoteText.value
);
newNoteText.value = genNoteText;
}
onMounted(async () => {
await accountStore.init();
await notesStore.fetchNotesForCurrentUser();
});
});
</script>
<template>
<div class="flex flex-col items-center max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8">
<div
class="flex flex-col items-center max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl md:text-6xl mb-4">Notes Dashboard</h2>
<h2
class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl md:text-6xl mb-4">
Notes Dashboard
</h2>
</div>
<div v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.READ_WRITE || activeMembership.access === ACCOUNT_ACCESS.ADMIN || activeMembership.access === ACCOUNT_ACCESS.OWNER)" class="w-full max-w-md mx-auto mb-3">
<textarea v-model="newNoteText" type="text" class="w-full rounded-l-md py-2 px-4 border-gray-400 border-2 focus:outline-none focus:border-blue-500" rows="5" placeholder="Add a note..."/>
<div
v-if="
activeMembership &&
(activeMembership.access === ACCOUNT_ACCESS.READ_WRITE ||
activeMembership.access === ACCOUNT_ACCESS.ADMIN ||
activeMembership.access === ACCOUNT_ACCESS.OWNER)
"
class="w-full max-w-md mx-auto mb-3">
<textarea
v-model="newNoteText"
type="text"
class="w-full rounded-l-md py-2 px-4 border-gray-400 border-2 focus:outline-none focus:border-blue-500"
rows="5"
placeholder="Add a note..." />
<div class="flex justify-evenly">
<button @click.prevent="addNote()" type="button"
<button
@click.prevent="addNote()"
type="button"
class="px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-opacity-50">
Add
</button>
<button v-if="activeMembership.account.features.includes('SPECIAL_FEATURE')" @click.prevent="genNote()" type="button"
<button
v-if="activeMembership.account.features.includes('SPECIAL_FEATURE')"
@click.prevent="genNote()"
type="button"
class="px-4 py-2 text-white bg-purple-600 rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50">
Gen
<Icon name="mdi:magic" class="h-6 w-6"/>
<Icon name="mdi:magic" class="h-6 w-6" />
</button>
</div>
</div>
<div class="w-full max-w-md">
<div v-for="note in notes" class="bg-white rounded-lg shadow-lg text-center px-6 py-8 md:mx-4 md:my-4">
<div
v-for="note in notes"
class="bg-white rounded-lg shadow-lg text-center px-6 py-8 md:mx-4 md:my-4">
<p class="text-gray-600 mb-4">{{ note.note_text }}</p>
<button @click.prevent="notesStore.deleteNote(note.id)"
v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.ADMIN || activeMembership.access === ACCOUNT_ACCESS.OWNER)"
<button
@click.prevent="notesStore.deleteNote(note.id)"
v-if="
activeMembership &&
(activeMembership.access === ACCOUNT_ACCESS.ADMIN ||
activeMembership.access === ACCOUNT_ACCESS.OWNER)
"
class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Delete
</button>

View File

@@ -1,8 +1,6 @@
<template>
<div class="prose lg:prose-xl m-5">
<p>
We are sorry that you were unable to subscribe.
</p>
<p>We are sorry that you were unable to subscribe.</p>
<p>
<NuxtLink to="/pricing">Pricing</NuxtLink>
</p>
@@ -10,4 +8,4 @@
<NuxtLink to="/dashboard">To Your Dashboard</NuxtLink>
</p>
</div>
</template>
</template>

View File

@@ -3,23 +3,30 @@
const config = useRuntimeConfig();
const notifyStore = useNotifyStore();
const loading = ref(false)
const email = ref('')
const loading = ref(false);
const email = ref('');
const sendResetPasswordLink = async () => {
try {
loading.value = true
const { data, error } = await supabase.auth.resetPasswordForEmail(email.value, {
redirectTo: `${config.public.siteRootUrl}/resetpassword`,
})
if (error) throw error
else notifyStore.notify("Password Reset link sent, check your email.", NotificationType.Success);
loading.value = true;
const { data, error } = await supabase.auth.resetPasswordForEmail(
email.value,
{
redirectTo: `${config.public.siteRootUrl}/resetpassword`
}
);
if (error) throw error;
else
notifyStore.notify(
'Password Reset link sent, check your email.',
NotificationType.Success
);
} catch (error) {
notifyStore.notify(error, NotificationType.Error);
} finally {
loading.value = false
loading.value = false;
}
}
};
</script>
<template>
<div class="flex flex-col items-center justify-center h-screen bg-gray-100">
@@ -28,11 +35,20 @@
<form @submit.prevent="sendResetPasswordLink" class="space-y-4">
<div>
<label for="email" class="block mb-2 font-bold">Email</label>
<input v-model="email" id="email" type="email" class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your email" required>
<input
v-model="email"
id="email"
type="email"
class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your email"
required />
</div>
<button :disabled="loading || email === ''" type="submit"
class="w-full py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700">Send Reset Password Link</button>
<button
:disabled="loading || email === ''"
type="submit"
class="w-full py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Send Reset Password Link
</button>
</form>
</div>
</div>

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
const user = useSupabaseUser()
const user = useSupabaseUser();
watchEffect(() => {
if (user.value) {
navigateTo('/dashboard', {replace: true})
navigateTo('/dashboard', { replace: true });
}
})
});
</script>
<template>
<div class="container mx-auto m-5">
@@ -13,57 +13,72 @@ const user = useSupabaseUser()
<div class="container mx-auto">
<div class="grid grid-cols-1 md:grid-cols-2 gap-16">
<div class="m-5">
<h1 class="text-5xl font-bold mb-4">
Build Your Next SaaS Faster
</h1>
<h1 class="text-5xl font-bold mb-4">Build Your Next SaaS Faster</h1>
<p class="text-gray-700 text-lg mb-8">
With SupaNuxt SaaS, you can easily get started building your
next web application. Our pre-configured tech stack and
industry leading features make it easy to get up and running in no time. Look! this guy is working so fast,
his hands are just a blur.. you could be this fast.
With SupaNuxt SaaS, you can easily get started building your next
web application. Our pre-configured tech stack and industry
leading features make it easy to get up and running in no time.
Look! this guy is working so fast, his hands are just a blur.. you
could be this fast.
</p>
<NuxtLink to="/signup" class="inline-block py-3 px-6 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Get Started</NuxtLink>
<NuxtLink
to="/signup"
class="inline-block py-3 px-6 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>Get Started</NuxtLink
>
</div>
<div>
<img src="~/assets/images/supanuxt_logo_200.png" alt="SupaNuxt SaaS Logo"/>
<img
src="~/assets/images/supanuxt_logo_200.png"
alt="SupaNuxt SaaS Logo" />
</div>
</div>
</div>
</section>
<section class="py-12">
<div class="container px-4 mx-auto">
<div class="flex flex-col md:flex-row items-center justify-center md:justify-between mb-8">
<div class="container px-4 mx-auto">
<div
class="flex flex-col md:flex-row items-center justify-center md:justify-between mb-8">
<h2 class="text-3xl font-bold mb-4 md:mb-0">Tech Stack</h2>
</div>
<div class="flex flex-col md:flex-row items-center mb-16">
<div class="md:w-full">
<ul class="grid grid-cols-3 gap-10 list-none">
<li>
<Icon name="skill-icons:nuxtjs-dark" class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">Nuxt 3</h3>
<p class="mt-2 text-base text-gray-500">The Progressive Vue.js Framework</p>
<p class="mt-2 text-base text-gray-500">
The Progressive Vue.js Framework
</p>
</li>
<li>
<Icon name="skill-icons:supabase-dark" class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">Supabase</h3>
<p class="mt-2 text-base text-gray-500">Auth including OAuth + Postgresql instance</p>
<p class="mt-2 text-base text-gray-500">
Auth including OAuth + Postgresql instance
</p>
</li>
<li>
<Icon name="skill-icons:postgresql-dark" class="h-12 w-12 mb-2" />
<Icon
name="skill-icons:postgresql-dark"
class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">PostgreSQL</h3>
<p class="mt-2 text-base text-gray-500">Relational Database</p>
</li>
<li>
<Icon name="logos:prisma" class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">Prisma</h3>
<p class="mt-2 text-base text-gray-500">Schema management + Strongly typed client</p>
<p class="mt-2 text-base text-gray-500">
Schema management + Strongly typed client
</p>
</li>
<li>
<Icon name="simple-icons:trpc" class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">TRPC</h3>
<p class="mt-2 text-base text-gray-500">Server/Client communication with Strong types, SSR compatible</p>
<p class="mt-2 text-base text-gray-500">
Server/Client communication with Strong types, SSR compatible
</p>
</li>
<li>
<Icon name="skill-icons:vuejs-dark" class="h-12 w-12 mb-2" />
@@ -73,104 +88,148 @@ const user = useSupabaseUser()
<li>
<Icon name="logos:stripe" class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">Stripe</h3>
<p class="mt-2 text-base text-gray-500">Payments including Webhook integration</p>
<p class="mt-2 text-base text-gray-500">
Payments including Webhook integration
</p>
</li>
<li>
<Icon name="skill-icons:tailwindcss-dark" class="h-12 w-12 mb-2" />
<Icon
name="skill-icons:tailwindcss-dark"
class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">Tailwind</h3>
<p class="mt-2 text-base text-gray-500">A utility-first CSS framework</p>
<p class="mt-2 text-base text-gray-500">
A utility-first CSS framework
</p>
</li>
<li>
<Icon name="skill-icons:vuejs-dark" class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">Vue.js</h3>
<p class="mt-2 text-base text-gray-500">The Progressive JavaScript Framework</p>
<p class="mt-2 text-base text-gray-500">
The Progressive JavaScript Framework
</p>
</li>
<li>
<Icon name="logos:openai-icon" class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">OpenAI</h3>
<p class="mt-2 text-base text-gray-500">AI Completions including Note generation from prompt</p>
<p class="mt-2 text-base text-gray-500">
AI Completions including Note generation from prompt
</p>
</li>
</ul>
</div>
</div>
<div class="flex flex-col md:flex-row items-center justify-center md:justify-between mb-8">
<div
class="flex flex-col md:flex-row items-center justify-center md:justify-between mb-8">
<h2 class="text-3xl font-bold mb-4 md:mb-0">Features</h2>
</div>
<!-- User Management (text left) -->
<div class="flex flex-col md:flex-row-reverse items-center mb-16">
<div class="md:w-1/2 md:ml-8 mb-8 md:mb-0">
<img src="~/assets/images/landing_user_management.jpeg" alt="User Management"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:ml-4">
<img
src="~/assets/images/landing_user_management.jpeg"
alt="User Management"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:ml-4" />
</div>
<div class="md:w-1/2">
<h3 class="text-xl font-bold mb-4">User Management</h3>
<p class="mb-4">SupaNuxt SaaS includes robust user management features, including
authentication with social login (oauth) or email/password, management of user roles and permissions, and
multi-user/team accounts that permit multiple users to share plan features including a team administration
facility and user roles within team. This is a great feature for businesses or community groups who want to
share the cost of the plan.</p>
<p class="mb-4">
SupaNuxt SaaS includes robust user management features, including
authentication with social login (oauth) or email/password,
management of user roles and permissions, and multi-user/team
accounts that permit multiple users to share plan features
including a team administration facility and user roles within
team. This is a great feature for businesses or community groups
who want to share the cost of the plan.
</p>
</div>
</div>
<!-- DB Schema (text right)-->
<div class="flex flex-col md:flex-row items-center mb-16">
<div class="md:w-1/2 md:mr-8 mb-8 md:mb-0">
<img src="~/assets/images/landing_db_schema_management.jpeg" alt="DB Schema Management"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:mr-4">
<img
src="~/assets/images/landing_db_schema_management.jpeg"
alt="DB Schema Management"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:mr-4" />
</div>
<div class="md:w-1/2">
<h3 class="text-xl font-bold mb-4">DB Schema Management</h3>
<p class="mb-4">We use Prisma for schema management to make sure you can easily
manage and keep track of your database schema. We also utilise Prisma based strong types which, with some help from TRPC, penetrate the entire stack all
the way to the web front end. This ensures that you can move fast with your feature development, alter schema and have those
type changes instantly available and validated everywhere.</p>
<p class="mb-4">
We use Prisma for schema management to make sure you can easily
manage and keep track of your database schema. We also utilise
Prisma based strong types which, with some help from TRPC,
penetrate the entire stack all the way to the web front end. This
ensures that you can move fast with your feature development,
alter schema and have those type changes instantly available and
validated everywhere.
</p>
</div>
</div>
<!-- Config (text left) -->
<div class="flex flex-col md:flex-row-reverse items-center mb-16">
<div class="md:w-1/2 md:ml-8 mb-8 md:mb-0">
<img src="~/assets/images/landing_config_environment.jpeg" alt="Config and Environment"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:ml-4">
<img
src="~/assets/images/landing_config_environment.jpeg"
alt="Config and Environment"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:ml-4" />
</div>
<div class="md:w-1/2">
<h3 class="text-xl font-bold mb-4">Config and Environment</h3>
<p class="mb-4">SupaNuxt SaaS includes an approach to config and environment
management that enables customisation and management of api keys.</p>
<p class="mb-4">
SupaNuxt SaaS includes an approach to config and environment
management that enables customisation and management of api keys.
</p>
</div>
</div>
<!-- State Management (text right)-->
<div class="flex flex-col md:flex-row items-center mb-16">
<div class="md:w-1/2 md:mr-8 mb-8 md:mb-0">
<img src="~/assets/images/landing_state_management.jpeg" alt="State Management"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:mr-4">
<img
src="~/assets/images/landing_state_management.jpeg"
alt="State Management"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:mr-4" />
</div>
<div class="md:w-1/2">
<h3 class="text-xl font-bold mb-4">State Management</h3>
<p class="mb-4">SupaNuxt SaaS includes multi modal state management that supports both Single Page Application (SPA)
pages such as dashboards and Server Side Rendered (SSR) style pages for public content that are crawlable by Search
engines like google and facilitate excellent Search Engine Optimisation (SEO).</p>
<p class="mb-4">
SupaNuxt SaaS includes multi modal state management that supports
both Single Page Application (SPA) pages such as dashboards and
Server Side Rendered (SSR) style pages for public content that are
crawlable by Search engines like google and facilitate excellent
Search Engine Optimisation (SEO).
</p>
</div>
</div>
<!-- Stripe (text left) -->
<div class="flex flex-col md:flex-row-reverse items-center mb-16">
<div class="md:w-1/2 md:ml-8 mb-8 md:mb-0">
<img src="~/assets/images/landing_stripe_integration.jpeg" alt="Stripe Integration"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:ml-4">
<img
src="~/assets/images/landing_stripe_integration.jpeg"
alt="Stripe Integration"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:ml-4" />
</div>
<div class="md:w-1/2">
<h3 class="text-xl font-bold mb-4">Stripe Integration</h3>
<p class="mb-4">SupaNuxt SaaS includes Stripe integration for subscription payments including
Subscription based support for multi pricing and multiple plans.</p>
<p class="mb-4">
SupaNuxt SaaS includes Stripe integration for subscription
payments including Subscription based support for multi pricing
and multiple plans.
</p>
</div>
</div>
<!-- Tailwind (text right)-->
<div class="flex flex-col md:flex-row items-center mb-16">
<div class="md:w-1/2 md:mr-8 mb-8 md:mb-0">
<img src="~/assets/images/landing_style_system.jpeg" alt="Style System"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:mr-4">
<img
src="~/assets/images/landing_style_system.jpeg"
alt="Style System"
class="w-full rounded-lg shadow-lg mb-4 md:mb-0 md:mr-4" />
</div>
<div class="md:w-1/2">
<h3 class="text-xl font-bold mb-4">Style System</h3>
<p class="mb-4">SupaNuxt SaaS includes Tailwind integration for site styling including a themable UI components with daisyUI</p>
<p class="mb-4">
SupaNuxt SaaS includes Tailwind integration for site styling
including a themable UI components with daisyUI
</p>
</div>
</div>
</div>

View File

@@ -10,7 +10,9 @@
// this could probably be an elegant destructure here but I lost patience
let account: AccountWithMembers | undefined;
if (join_password) {
const result = await $client.account.getAccountByJoinPassword.useQuery({ join_password });
const result = await $client.account.getAccountByJoinPassword.useQuery({
join_password
});
account = result.data.value?.account;
}
@@ -19,45 +21,51 @@
async function doJoin() {
if (account) {
await accountStore.joinUserToAccountPending(account.id);
navigateTo('/dashboard', {replace: true})
navigateTo('/dashboard', { replace: true });
} else {
console.log(`Unable to Join`)
console.log(`Unable to Join`);
}
}
</script>
<template>
<div class="py-10 px-4 sm:px-6 lg:px-8">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-extrabold text-gray-900">Request to Join {{ account?.name }}</h2>
<h2 class="text-3xl font-extrabold text-gray-900">
Request to Join {{ account?.name }}
</h2>
<template v-if="dbUser?.dbUser">
<p class="mt-2 text-sm text-gray-500">
Click below to request to Join the team.
Your request to join will remain as 'Pending'
untill the team administrators complete their review.</p>
<p class="mt-2 text-sm text-gray-500">
If your requeste is approved, you will become a member of the team and
will be able to switch to the team account at any time in order to share
the benefits of the team plan.
Click below to request to Join the team. Your request to join will
remain as 'Pending' untill the team administrators complete their
review.
</p>
<p class="mt-2 text-sm text-gray-500">
If your requeste is approved, you will become a member of the team and
will be able to switch to the team account at any time in order to
share the benefits of the team plan.
</p>
<div class="mt-6">
<button @click.prevent="doJoin()" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md
shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<button
@click.prevent="doJoin()"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Join
</button>
</div>
</template>
<template v-else>
<p class="m-5 text-sm text-gray-500">Only signed in users can join a team. Please either Signup or Signin and then return to this page using the join link.</p>
<button @click.prevent="navigateTo('/signup')" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md
shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<p class="m-5 text-sm text-gray-500">
Only signed in users can join a team. Please either Signup or Signin
and then return to this page using the join link.
</p>
<button
@click.prevent="navigateTo('/signup')"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Sign Up
</button>
<div class="m-10"></div>
<button @click.prevent="navigateTo('/signin')" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md
shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<button
@click.prevent="navigateTo('/signin')"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Sign In
</button>
</template>

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
const route = useRoute();
const { $client } = useNuxtApp();
const { data: note } = await $client.notes.getById.useQuery({note_id: +route.params.note_id});
const route = useRoute();
const { $client } = useNuxtApp();
const { data: note } = await $client.notes.getById.useQuery({
note_id: +route.params.note_id
});
</script>
<template>
<div class="prose lg:prose-xl m-5">
<h3>Note Detail {{ route.params.note_id }}</h3>
<div class="prose lg:prose-xl m-5">{{ note?.note.note_text }}</div>
</div>
</template>

View File

@@ -2,7 +2,7 @@
import { storeToRefs } from 'pinia';
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
const accountStore = useAccountStore()
const accountStore = useAccountStore();
const { activeMembership } = storeToRefs(accountStore);
onMounted(async () => {
@@ -10,79 +10,117 @@
});
</script>
<template>
<div class="flex flex-col items-center max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8">
<div
class="flex flex-col items-center max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl md:text-6xl mb-4">Flexible Pricing</h2>
<p class="text-xl text-gray-500">SupaNuxt SaaS is completely free and open source but you can price your own SaaS like this</p>
<h2
class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl md:text-6xl mb-4">
Flexible Pricing
</h2>
<p class="text-xl text-gray-500">
SupaNuxt SaaS is completely free and open source but you can price your
own SaaS like this
</p>
</div>
<div class="flex flex-col md:flex-row justify-center items-center">
<!-- Free Plan -->
<div class="bg-white rounded-lg shadow-lg text-center px-6 py-8 md:mx-4 md:my-4 md:flex-1">
<div
class="bg-white rounded-lg shadow-lg text-center px-6 py-8 md:mx-4 md:my-4 md:flex-1">
<h3 class="text-2xl font-bold text-gray-900 mb-4">Free Plan</h3>
<p class="text-gray-600 mb-4">Single user, 10 notes</p>
<p class="text-3xl font-bold text-gray-900 mb-4">$0<span class="text-gray-600 text-lg">/mo</span></p>
<button
v-if="!activeMembership"
@click.prevent="navigateTo('/signup')"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Start for free
<p class="text-3xl font-bold text-gray-900 mb-4">
$0<span class="text-gray-600 text-lg">/mo</span>
</p>
<button
v-if="!activeMembership"
@click.prevent="navigateTo('/signup')"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Start for free
</button>
</div>
<!-- Personal Plan -->
<div class="bg-white rounded-lg shadow-lg text-center px-6 py-8 md:mx-4 md:my-4 md:flex-1">
<div
class="bg-white rounded-lg shadow-lg text-center px-6 py-8 md:mx-4 md:my-4 md:flex-1">
<h3 class="text-2xl font-bold text-gray-900 mb-4">Personal Plan</h3>
<p class="text-gray-600 mb-4">Single user, 100 notes</p>
<p class="text-3xl font-bold text-gray-900 mb-4">$15<span class="text-gray-600 text-lg">/mo</span></p>
<p class="text-3xl font-bold text-gray-900 mb-4">
$15<span class="text-gray-600 text-lg">/mo</span>
</p>
<!-- logged in user gets a subscribe button-->
<form
action="/create-checkout-session"
<form
action="/create-checkout-session"
method="POST"
v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access !== ACCOUNT_ACCESS.ADMIN) && (activeMembership?.account.plan_name !== 'Individual Plan')">
<input type="hidden" name="price_id" value="price_1MpOiwJfLn4RhYiLqfy6U8ZR" />
<input type="hidden" name="account_id" :value="activeMembership?.account_id" />
<button
type="submit"
v-if="
activeMembership &&
(activeMembership.access === ACCOUNT_ACCESS.OWNER ||
activeMembership.access !== ACCOUNT_ACCESS.ADMIN) &&
activeMembership?.account.plan_name !== 'Individual Plan'
">
<input
type="hidden"
name="price_id"
value="price_1MpOiwJfLn4RhYiLqfy6U8ZR" />
<input
type="hidden"
name="account_id"
:value="activeMembership?.account_id" />
<button
type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Subscribe
</button>
</form>
<!-- anon user gets a link to sign up -->
<button
v-if="!activeMembership"
@click.prevent="navigateTo('/signup')"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Get started
<button
v-if="!activeMembership"
@click.prevent="navigateTo('/signup')"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Get started
</button>
</div>
<!-- Team Plan -->
<div class="bg-white rounded-lg shadow-lg text-center px-6 py-8 md:mx-4 md:my-4 md:flex-1">
<div
class="bg-white rounded-lg shadow-lg text-center px-6 py-8 md:mx-4 md:my-4 md:flex-1">
<h3 class="text-2xl font-bold text-gray-900 mb-4">Team Plan</h3>
<p class="text-gray-600 mb-4">10 users, 200 notes</p>
<p class="text-3xl font-bold text-gray-900 mb-4">$25<span class="text-gray-600 text-lg">/mo</span></p>
<p class="text-3xl font-bold text-gray-900 mb-4">
$25<span class="text-gray-600 text-lg">/mo</span>
</p>
<!-- logged in user gets a subscribe button-->
<form
action="/create-checkout-session"
<form
action="/create-checkout-session"
method="POST"
v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access !== ACCOUNT_ACCESS.ADMIN) && (activeMembership?.account.plan_name !== 'Team Plan')">
<input type="hidden" name="price_id" value="price_1MpOjtJfLn4RhYiLsjzAso90" />
<input type="hidden" name="account_id" :value="activeMembership?.account_id" />
<button
type="submit"
v-if="
activeMembership &&
(activeMembership.access === ACCOUNT_ACCESS.OWNER ||
activeMembership.access !== ACCOUNT_ACCESS.ADMIN) &&
activeMembership?.account.plan_name !== 'Team Plan'
">
<input
type="hidden"
name="price_id"
value="price_1MpOjtJfLn4RhYiLsjzAso90" />
<input
type="hidden"
name="account_id"
:value="activeMembership?.account_id" />
<button
type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Subscribe
</button>
</form>
<!-- anon user gets a link to sign up -->
<button
v-if="!activeMembership"
@click.prevent="navigateTo('/signup')"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Get started
<button
v-if="!activeMembership"
@click.prevent="navigateTo('/signup')"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue">
Get started
</button>
</div>
</div>

View File

@@ -2,53 +2,83 @@
<div class="prose lg:prose-xl m-5">
<h1>Privacy Statement</h1>
<p>Your privacy is important to us. This privacy statement explains what personal data we collect from you and how we
use it. By using our website, you agree to the terms of this privacy statement.</p>
<p>
Your privacy is important to us. This privacy statement explains what
personal data we collect from you and how we use it. By using our website,
you agree to the terms of this privacy statement.
</p>
<h2>Information we collect</h2>
<p>We collect personal information that you voluntarily provide to us when you use our website, including your email
and full name. We also collect non-personal information about your use of our website, such as your IP address,
browser type, and the pages you visit.</p>
<p>
We collect personal information that you voluntarily provide to us when
you use our website, including your email and full name. We also collect
non-personal information about your use of our website, such as your IP
address, browser type, and the pages you visit.
</p>
<p>In addition to the personal data that we collect directly from you, we also use a third-party authentication
provider called Supabase to manage user authentication. When you use our website, Supabase may collect personal data
about you, such as your email address and authentication credentials. For more information about Supabase's data
practices, please refer to their privacy policy at <a
href="https://supabase.com/privacy">https://supabase.com/privacy</a>.</p>
<p>
In addition to the personal data that we collect directly from you, we
also use a third-party authentication provider called Supabase to manage
user authentication. When you use our website, Supabase may collect
personal data about you, such as your email address and authentication
credentials. For more information about Supabase's data practices, please
refer to their privacy policy at
<a href="https://supabase.com/privacy">https://supabase.com/privacy</a>.
</p>
<h2>How we use your information</h2>
<p>We use your personal information to provide you with the products and services you request, to communicate with
you, and to improve our website. We may also use your information for marketing purposes, but we will always give
you the option to opt-out of receiving marketing communications from us.</p>
<p>
We use your personal information to provide you with the products and
services you request, to communicate with you, and to improve our website.
We may also use your information for marketing purposes, but we will
always give you the option to opt-out of receiving marketing
communications from us.
</p>
<h2>Disclosure of your information</h2>
<p>We may disclose your personal information to third parties who provide services to us, such as website hosting,
data analysis, and customer service. We may also disclose your information if we believe it is necessary to comply
with the law or to protect our rights or the rights of others.</p>
<p>
We may disclose your personal information to third parties who provide
services to us, such as website hosting, data analysis, and customer
service. We may also disclose your information if we believe it is
necessary to comply with the law or to protect our rights or the rights of
others.
</p>
<p>As mentioned above, we use a third-party authentication provider called Supabase to manage user authentication.
Supabase may share your personal data with other third-party service providers that they use to provide their
services, such as hosting and cloud storage providers. For more information about Supabase's data sharing practices,
please refer to their privacy policy at <a href="https://supabase.com/privacy">https://supabase.com/privacy</a>.</p>
<p>
As mentioned above, we use a third-party authentication provider called
Supabase to manage user authentication. Supabase may share your personal
data with other third-party service providers that they use to provide
their services, such as hosting and cloud storage providers. For more
information about Supabase's data sharing practices, please refer to their
privacy policy at
<a href="https://supabase.com/privacy">https://supabase.com/privacy</a>.
</p>
<h2>Security of your information</h2>
<p>We take reasonable measures to protect your personal information from unauthorized access, use, or disclosure.
However, no data transmission over the internet or electronic storage is completely secure, so we cannot guarantee
the absolute security of your information.</p>
<p>
We take reasonable measures to protect your personal information from
unauthorized access, use, or disclosure. However, no data transmission
over the internet or electronic storage is completely secure, so we cannot
guarantee the absolute security of your information.
</p>
<h2>Changes to this privacy statement</h2>
<p>We may update this privacy statement from time to time. Any changes will be posted on this page, so please check
back periodically to review the most current version of the statement.</p>
<p>
We may update this privacy statement from time to time. Any changes will
be posted on this page, so please check back periodically to review the
most current version of the statement.
</p>
<h2>Contact us</h2>
<p>If you have any questions or concerns about our privacy practices, please contact us at [insert contact
information].</p>
<p>
If you have any questions or concerns about our privacy practices, please
contact us at [insert contact information].
</p>
</div>
</template>
</template>

View File

@@ -3,27 +3,27 @@
const notifyStore = useNotifyStore();
const loading = ref(false)
const password = ref('')
const confirmPassword = ref('')
const loading = ref(false);
const password = ref('');
const confirmPassword = ref('');
const changePassword = async () => {
try {
loading.value = true
loading.value = true;
const { data, error } = await supabase.auth.updateUser({
password: password.value
});
if (error) throw error
if (error) throw error;
else {
notifyStore.notify("password changed", NotificationType.Success);
navigateTo('/signin', {replace: true}); // navigate to signin because it is best practice although the auth session seems to be valid so it immediately redirects to dashboard
notifyStore.notify('password changed', NotificationType.Success);
navigateTo('/signin', { replace: true }); // navigate to signin because it is best practice although the auth session seems to be valid so it immediately redirects to dashboard
}
} catch (error) {
notifyStore.notify(error, NotificationType.Error);
} finally {
loading.value = false
loading.value = false;
}
}
};
</script>
<template>
<div class="flex flex-col items-center justify-center h-screen bg-gray-100">
@@ -31,17 +31,35 @@
<h1 class="text-3xl font-bold text-center">Forgot Pasword</h1>
<form @submit.prevent="changePassword" class="space-y-4">
<div>
<label for="password" class="block mb-2 font-bold">New Password</label>
<input v-model="password" id="password" type="password" class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your new password" required>
<label for="password" class="block mb-2 font-bold"
>New Password</label
>
<input
v-model="password"
id="password"
type="password"
class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your new password"
required />
</div>
<div>
<label for="confirmPassword" class="block mb-2 font-bold">Confirm New Password</label>
<input v-model="confirmPassword" id="confirmPassword" type="password" class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Confirm new password" required>
<label for="confirmPassword" class="block mb-2 font-bold"
>Confirm New Password</label
>
<input
v-model="confirmPassword"
id="confirmPassword"
type="password"
class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Confirm new password"
required />
</div>
<button :disabled="loading || password === '' || (confirmPassword !== password)" type="submit"
class="w-full py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700">Change Password</button>
<button
:disabled="loading || password === '' || confirmPassword !== password"
type="submit"
class="w-full py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Change Password
</button>
</form>
</div>
</div>

View File

@@ -1,47 +1,53 @@
<script setup lang="ts">
const user = useSupabaseUser()
const user = useSupabaseUser();
const supabase = useSupabaseAuthClient();
const accountStore = useAccountStore()
const accountStore = useAccountStore();
const notifyStore = useNotifyStore();
const loading = ref(false)
const email = ref('')
const password = ref('')
const loading = ref(false);
const email = ref('');
const password = ref('');
const handleStandardSignin = async () => {
console.log(`handleStandardSignin email.value:${email.value}, password.value:${password.value}`);
console.log(
`handleStandardSignin email.value:${email.value}, password.value:${password.value}`
);
try {
loading.value = true
const { error } = await supabase.auth.signInWithPassword({ email: email.value, password: password.value })
if (error) throw error
loading.value = true;
const { error } = await supabase.auth.signInWithPassword({
email: email.value,
password: password.value
});
if (error) throw error;
} catch (error) {
notifyStore.notify(error, NotificationType.Error);
} finally {
loading.value = false
loading.value = false;
}
}
};
const handleGoogleSignin = async () => {
console.log('handleGoogleSignin');
try {
loading.value = true
const { error } = await supabase.auth.signInWithOAuth({ provider: 'google' })
if (error) throw error
loading.value = true;
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google'
});
if (error) throw error;
} catch (error) {
notifyStore.notify(error, NotificationType.Error);
} finally {
loading.value = false
loading.value = false;
}
}
};
watchEffect(async () => {
if (user.value) {
await accountStore.init();
navigateTo('/dashboard', {replace: true})
navigateTo('/dashboard', { replace: true });
}
})
});
</script>
<template>
<div class="flex flex-col items-center justify-center h-screen bg-gray-100">
@@ -50,20 +56,40 @@
<form @submit.prevent="handleStandardSignin" class="space-y-4">
<div>
<label for="email" class="block mb-2 font-bold">Email</label>
<input v-model="email" id="email" type="email" class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your email" required>
<input
v-model="email"
id="email"
type="email"
class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your email"
required />
</div>
<div>
<label for="password" class="block mb-2 font-bold">Password</label>
<input v-model="password" id="password" type="password" class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your password" required>
<input
v-model="password"
id="password"
type="password"
class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your password"
required />
</div>
<NuxtLink id="forgotPasswordLink" to="/forgotpassword" class="text-right block">Forgot your password?</NuxtLink>
<button :disabled="loading || password === ''" type="submit"
class="w-full py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700">Sign in</button>
<NuxtLink
id="forgotPasswordLink"
to="/forgotpassword"
class="text-right block"
>Forgot your password?</NuxtLink
>
<button
:disabled="loading || password === ''"
type="submit"
class="w-full py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Sign in
</button>
</form>
<p class="text-center">or</p>
<button @click="handleGoogleSignin()"
<button
@click="handleGoogleSignin()"
class="w-full py-2 text-white bg-red-600 rounded-md hover:bg-red-700">
<span class="flex items-center justify-center space-x-2">
<Icon name="fa-brands:google" class="w-5 h-5" />

View File

@@ -1,37 +1,39 @@
<script setup lang="ts">
const user = useSupabaseUser()
const user = useSupabaseUser();
const supabase = useSupabaseAuthClient();
const notifyStore = useNotifyStore();
const loading = ref(false)
const email = ref('')
const password = ref('')
const confirmPassword = ref('')
const signUpOk = ref(false)
const loading = ref(false);
const email = ref('');
const password = ref('');
const confirmPassword = ref('');
const signUpOk = ref(false);
const handleStandardSignup = async () => {
try {
loading.value = true
const { data, error } = await supabase.auth.signUp({ email: email.value, password: password.value })
loading.value = true;
const { data, error } = await supabase.auth.signUp({
email: email.value,
password: password.value
});
if (error) {
throw error
}
else {
throw error;
} else {
signUpOk.value = true;
}
} catch (error) {
notifyStore.notify(error, NotificationType.Error);
} finally {
loading.value = false
loading.value = false;
}
}
};
watchEffect(() => {
if (user.value) {
navigateTo('/dashboard', { replace: true })
navigateTo('/dashboard', { replace: true });
}
})
});
</script>
<template>
<div class="flex flex-col items-center justify-center h-screen bg-gray-100">
@@ -40,26 +42,51 @@
<form @submit.prevent="handleStandardSignup" class="space-y-4">
<div>
<label for="email" class="block mb-2 font-bold">Email</label>
<input v-model="email" id="email" type="email" class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your email" required>
<input
v-model="email"
id="email"
type="email"
class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your email"
required />
</div>
<div>
<label for="password" class="block mb-2 font-bold">Password</label>
<input v-model="password" id="password" type="password" class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your password" required>
<input
v-model="password"
id="password"
type="password"
class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Enter your password"
required />
</div>
<div>
<label for="confirmPassword" class="block mb-2 font-bold">Confirm Password</label>
<input v-model="confirmPassword" id="confirmPassword" type="password"
class="w-full p-2 border border-gray-400 rounded-md" placeholder="Confirm your password" required>
<label for="confirmPassword" class="block mb-2 font-bold"
>Confirm Password</label
>
<input
v-model="confirmPassword"
id="confirmPassword"
type="password"
class="w-full p-2 border border-gray-400 rounded-md"
placeholder="Confirm your password"
required />
</div>
<button :disabled="loading || password === '' || (confirmPassword !== password)" type="submit"
class="w-full py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700">Sign up</button>
<p v-if="signUpOk" class="mt-4 text-lg text-center">You have successfully signed up. Please check your email for a link to confirm your email address and proceed.</p>
<button
:disabled="loading || password === '' || confirmPassword !== password"
type="submit"
class="w-full py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Sign up
</button>
<p v-if="signUpOk" class="mt-4 text-lg text-center">
You have successfully signed up. Please check your email for a link to
confirm your email address and proceed.
</p>
</form>
<p class="text-center">or</p>
<button @click="supabase.auth.signInWithOAuth({ provider: 'google' })"
<button
@click="supabase.auth.signInWithOAuth({ provider: 'google' })"
class="w-full py-2 text-white bg-red-600 rounded-md hover:bg-red-700">
<span class="flex items-center justify-center space-x-2">
<Icon name="fa-brands:google" class="w-5 h-5" />
@@ -67,8 +94,9 @@
</span>
</button>
<p class="mt-4 text-xs text-center text-gray-500">
By proceeding, I agree to the <NuxtLink to="/privacy">Privacy Statement</NuxtLink> and <NuxtLink to="/terms">Terms
of Service</NuxtLink>
By proceeding, I agree to the
<NuxtLink to="/privacy">Privacy Statement</NuxtLink> and
<NuxtLink to="/terms">Terms of Service</NuxtLink>
</p>
</div>
</div>

View File

@@ -1,23 +1,31 @@
<script setup lang="ts">
import Stripe from 'stripe';
const config = useRuntimeConfig();
const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' });
const route = useRoute();
let customer: Stripe.Response<Stripe.Customer | Stripe.DeletedCustomer>
try{
const session = await stripe.checkout.sessions.retrieve(route?.query?.session_id as string);
customer = await stripe.customers.retrieve(session?.customer as string);
} catch(e) {
console.log(`Error ${e}`)
}
import Stripe from 'stripe';
const config = useRuntimeConfig();
const stripe = new Stripe(config.stripeSecretKey, {
apiVersion: '2022-11-15'
});
const route = useRoute();
let customer: Stripe.Response<Stripe.Customer | Stripe.DeletedCustomer>;
try {
const session = await stripe.checkout.sessions.retrieve(
route?.query?.session_id as string
);
customer = await stripe.customers.retrieve(session?.customer as string);
} catch (e) {
console.log(`Error ${e}`);
}
</script>
<template>
<div class="prose lg:prose-xl m-5">
<p>
<span v-if="customer && !customer.deleted">We appreciate your business {{customer.name}}!</span>
<span v-if="customer && customer.deleted">It appears your stripe customer information has been deleted!</span>
<span v-if="customer && !customer.deleted">
We appreciate your business {{ customer.name }}!
</span>
<span v-if="customer && customer.deleted">
It appears your stripe customer information has been deleted!
</span>
</p>
<p>Go to Your <NuxtLink to="/dashboard">Dashboard</NuxtLink></p>
</div>
</template>
</template>

View File

@@ -2,57 +2,95 @@
<div class="prose lg:prose-xl m-5">
<h1>Terms of Service</h1>
<p>These terms of service (the "Agreement") govern your use of our website and services (collectively, the
"Service"). By using the Service, you agree to be bound by the terms of this Agreement. If you do not agree to
these terms, you may not use the Service.</p>
<p>
These terms of service (the "Agreement") govern your use of our website
and services (collectively, the "Service"). By using the Service, you
agree to be bound by the terms of this Agreement. If you do not agree to
these terms, you may not use the Service.
</p>
<h2>Use of the Service</h2>
<p>You may use the Service only for lawful purposes and in accordance with this Agreement. You agree not to use the
Service:</p>
<p>
You may use the Service only for lawful purposes and in accordance with
this Agreement. You agree not to use the Service:
</p>
<ul>
<li>In any way that violates any applicable federal, state, local, or international law or regulation</li>
<li>To impersonate or attempt to impersonate us, our employees, another user, or any other person or entity</li>
<li>To engage in any other conduct that restricts or inhibits anyone's use or enjoyment of the Service, or which,
as determined by us, may harm us or users of the Service or expose them to liability</li>
<li>
In any way that violates any applicable federal, state, local, or
international law or regulation
</li>
<li>
To impersonate or attempt to impersonate us, our employees, another
user, or any other person or entity
</li>
<li>
To engage in any other conduct that restricts or inhibits anyone's use
or enjoyment of the Service, or which, as determined by us, may harm us
or users of the Service or expose them to liability
</li>
</ul>
<h2>Intellectual Property</h2>
<p>The Service and its entire contents, features, and functionality (including but not limited to all information,
software, text, displays, images, video, and audio, and the design, selection, and arrangement thereof) are owned
by us, our licensors, or other providers of such material and are protected by United States and international
copyright, trademark, patent, trade secret, and other intellectual property or proprietary rights laws.</p>
<p>
The Service and its entire contents, features, and functionality
(including but not limited to all information, software, text, displays,
images, video, and audio, and the design, selection, and arrangement
thereof) are owned by us, our licensors, or other providers of such
material and are protected by United States and international copyright,
trademark, patent, trade secret, and other intellectual property or
proprietary rights laws.
</p>
<p>You may not reproduce, distribute, modify, create derivative works of, publicly display, publicly perform,
republish, download, store, or transmit any of the material on our website, except as follows:</p>
<p>
You may not reproduce, distribute, modify, create derivative works of,
publicly display, publicly perform, republish, download, store, or
transmit any of the material on our website, except as follows:
</p>
<ul>
<li>Your computer may temporarily store copies of such materials in RAM incidental to your accessing and viewing
those materials</li>
<li>You may store files that are automatically cached by your Web browser for display enhancement purposes</li>
<li>You may print or download one copy of a reasonable number of pages of the website for your own personal,
non-commercial use and not for further reproduction, publication, or distribution</li>
<li>
Your computer may temporarily store copies of such materials in RAM
incidental to your accessing and viewing those materials
</li>
<li>
You may store files that are automatically cached by your Web browser
for display enhancement purposes
</li>
<li>
You may print or download one copy of a reasonable number of pages of
the website for your own personal, non-commercial use and not for
further reproduction, publication, or distribution
</li>
</ul>
<h2>Disclaimer of Warranties</h2>
<p>The Service is provided on an "as is" and "as available" basis, without any warranties of any kind, either
express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose,
or non-infringement. We make no warranty that the Service will meet your requirements, be available on an
uninterrupted, secure, or error-free basis, or be free from viruses or other harmful components.</p>
<p>
The Service is provided on an "as is" and "as available" basis, without
any warranties of any kind, either express or implied, including but not
limited to warranties of merchantability, fitness for a particular
purpose, or non-infringement. We make no warranty that the Service will
meet your requirements, be available on an uninterrupted, secure, or
error-free basis, or be free from viruses or other harmful components.
</p>
<h2>Limitation of Liability</h2>
<p>In no event shall we be liable for any direct, indirect, incidental, special, or consequential damages arising
out of or in any way connected with the use of the Service, whether based on contract, tort, strict liability, or
any other theory of liability.</p>
<p>
In no event shall we be liable for any direct, indirect, incidental,
special, or consequential damages arising out of or in any way connected
with the use of the Service, whether based on contract, tort, strict
liability, or any other theory of liability.
</p>
<h2>Indemnification</h2>
<p>You agree to defend, indemnify, and hold us harmless from and against any claims, liabilities, damages,
judgments, fines, costs, and expenses.</p>
<p>
You agree to defend, indemnify, and hold us harmless from and against any
claims, liabilities, damages, judgments, fines, costs, and expenses.
</p>
</div>
</template>