refactor components and notifications bug

This commit is contained in:
Michael Dausmann
2023-10-06 19:16:37 +11:00
parent a64bdc19ab
commit 4cb78b0060
13 changed files with 174 additions and 92 deletions

View File

@@ -1,19 +1,30 @@
# Changelog # Changelog
## Version 1.4.1
- Refactor some components and explicitly split out client only components
- Fix bug in the notifications
- Update readme to indicate sister project in react/next
## Version 1.4.0 ## Version 1.4.0
- Cookie Consent - Cookie Consent
```npm i vanilla-cookieconsent``` `npm i vanilla-cookieconsent`
## Version 1.3.0 ## Version 1.3.0
- Add an example of usage limits (Notes AI Gen). - Add an example of usage limits (Notes AI Gen).
- Includes non-destructive schema changes - Includes non-destructive schema changes
```npx prisma db push``` `npx prisma db push`
## Version 1.2.0 ## Version 1.2.0
- 'Lift' auth context into server middleware to support authenticated api (rest) endpoints for alternate clients while still supporting fully typed Trpc context. - 'Lift' auth context into server middleware to support authenticated api (rest) endpoints for alternate clients while still supporting fully typed Trpc context.
## Version 1.1.0 ## Version 1.1.0
- Upgrade Prisma to version 5 to improve performance (https://www.prisma.io/docs/guides/upgrade-guides/upgrading-versions/upgrading-to-prisma-5) - Upgrade Prisma to version 5 to improve performance (https://www.prisma.io/docs/guides/upgrade-guides/upgrading-versions/upgrading-to-prisma-5)
``` ```
npm install @prisma/client@5 npm install @prisma/client@5
npm install -D prisma@5 npm install -D prisma@5
@@ -21,17 +32,21 @@ npx prisma generate
``` ```
- Upgrade Nuxt to 3.7.0 - Upgrade Nuxt to 3.7.0
``` ```
npx nuxi upgrade --force npx nuxi upgrade --force
``` ```
## Version 1.0.0 ## Version 1.0.0
First Release version. If your package.json does not have a version attribute, this is the version you have.
First Release version. If your package.json does not have a version attribute, this is the version you have.
## Project Creation (for interest only) ## Project Creation (for interest only)
This is what I did to create the project including all the extra fiddly stuff. Putting this here so I don't forget.
This is what I did to create the project including all the extra fiddly stuff. Putting this here so I don't forget.
### Setup Nuxt ### Setup Nuxt
I Followed instructions from here https://nuxt.com/docs/getting-started/installation I Followed instructions from here https://nuxt.com/docs/getting-started/installation
```bash ```bash
@@ -42,6 +57,7 @@ code nuxt3-boilerplate/
npm install npm install
npm run dev -- -o npm run dev -- -o
``` ```
### Setup Supabase ### Setup Supabase
To setup supabase and middleware, loosely follow instructions from https://www.youtube.com/watch?v=IcaL1RfnU44 To setup supabase and middleware, loosely follow instructions from https://www.youtube.com/watch?v=IcaL1RfnU44
@@ -53,18 +69,22 @@ npm install @nuxtjs/supabase
``` ```
add this to nuxt.config.ts add this to nuxt.config.ts
``` ```
modules: ['@nuxtjs/supabase'] modules: ['@nuxtjs/supabase']
``` ```
### Setup Google OAuth ### Setup Google OAuth
Follow these instructions to add google oath https://supabase.com/docs/guides/auth/social-login/auth-google Follow these instructions to add google oath https://supabase.com/docs/guides/auth/social-login/auth-google
### Nuxt-Supabase ### Nuxt-Supabase
Then I frigged around trying to get the nuxt-supabase module to work properly for the oauth flow. It's a bit of a mess TBH. Eventually I looked at the demo https://github.com/nuxt-modules/supabase/tree/main/demo like a chump and got it working
Then I frigged around trying to get the nuxt-supabase module to work properly for the oauth flow. It's a bit of a mess TBH. Eventually I looked at the demo https://github.com/nuxt-modules/supabase/tree/main/demo like a chump and got it working
### Integrating Prisma ### Integrating Prisma
This felt like a difficult decision at first. the Subabase client has some pseudo sql Ormy sort of features already
This felt like a difficult decision at first. the Subabase client has some pseudo sql Ormy sort of features already
but Prisma has this awesome schema management support and autogeneration of a typed client works great and reduces errors. but Prisma has this awesome schema management support and autogeneration of a typed client works great and reduces errors.
I already had a schema lying around based on this (https://blog.checklyhq.com/building-a-multi-tenant-saas-data-model/) that was nearly what I needed and it was nice to be able to re-use it. I already had a schema lying around based on this (https://blog.checklyhq.com/building-a-multi-tenant-saas-data-model/) that was nearly what I needed and it was nice to be able to re-use it.
@@ -72,7 +92,8 @@ I already had a schema lying around based on this (https://blog.checklyhq.com/bu
npm install prisma --save-dev npm install prisma --save-dev
npx prisma init npx prisma init
``` ```
go to Supabase -> settings -> database -> connection string -> URI.. and copy the URI into the
go to Supabase -> settings -> database -> connection string -> URI.. and copy the URI into the
DATABASE_URL setting created with prisma init. DATABASE_URL setting created with prisma init.
still in database, go to 'Database password' and reset/set it and copy the password into the [YOUR-PASSWORD] placeholder in the URI still in database, go to 'Database password' and reset/set it and copy the password into the [YOUR-PASSWORD] placeholder in the URI
@@ -85,10 +106,12 @@ npx prisma generate
``` ```
### Stripe Integration ### Stripe Integration
This was a royal pain in the butt. Got some tips from https://github.com/jurassicjs/nuxt3-fullstack-tutorial and https://www.youtube.com/watch?v=A24aKCQ-rf4&t=895s Official docs try to be helpful but succeed only in confusing things https://stripe.com/docs/billing/quickstart
I set up a Stripe account with a couple of 'Products' with a single price each to represent my different plans. These price id's are embedded into the Pricing page. This was a royal pain in the butt. Got some tips from https://github.com/jurassicjs/nuxt3-fullstack-tutorial and https://www.youtube.com/watch?v=A24aKCQ-rf4&t=895s Official docs try to be helpful but succeed only in confusing things https://stripe.com/docs/billing/quickstart
I set up a Stripe account with a couple of 'Products' with a single price each to represent my different plans. These price id's are embedded into the Pricing page.
### Key things I learned ### Key things I learned
- You need to need to pre-emptively create a Stripe user *before* you send them to the checkout page so that you know who they are when the webhook comes back.
- There are like a Billion Fricking Webhooks you *can* subscribe to but for an MVP, you just need the *customer.subscription* events and you basically treat them all the same. - You need to need to pre-emptively create a Stripe user _before_ you send them to the checkout page so that you know who they are when the webhook comes back.
- There are like a Billion Fricking Webhooks you _can_ subscribe to but for an MVP, you just need the _customer.subscription_ events and you basically treat them all the same.

View File

@@ -8,6 +8,9 @@ Demo site [here](https://nuxt3-saas-boilerplate.netlify.app/)
Pottery Helper [here](https://potteryhelper.com/) Pottery Helper [here](https://potteryhelper.com/)
## Sister Project using React + Next 13
Sick of Vue.js/Nuxt3, why not checkout React/Next sister project [SupaNext SaaS](https://github.com/JavascriptMick/supanext-saas)
## Community ## Community
Discord [here](https://discord.gg/3hWPDTA4kD) Discord [here](https://discord.gg/3hWPDTA4kD)

View File

@@ -1,43 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia';
const supabase = useSupabaseAuthClient();
const user = useSupabaseUser(); const user = useSupabaseUser();
const accountStore = useAccountStore()
const { dbUser, activeAccountId } = storeToRefs(accountStore);
const notifyStore = useNotifyStore();
const { notifications } = storeToRefs(notifyStore);
onMounted(async () => {
await accountStore.init()
});
async function signout() {
await supabase.auth.signOut();
if(accountStore){
accountStore.signout();
}
navigateTo('/', {replace: true});
}
</script> </script>
<template> <template>
<div class="navbar bg-base-100"> <div class="navbar bg-base-100">
<div class="toast toast-end toast-top"> <Notifications/>
<div v-for="notification in notifications" :class="notification.type">
<div>
<button
@click.prevent="notifyStore.removeNotification(notification)"
type="button"
class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
aria-label="Close">
<span class="sr-only">Close</span>
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</button>
<span>&nbsp;{{notification.message}}</span>
</div>
</div>
</div>
<div class="navbar-start"> <div class="navbar-start">
<div class="dropdown"> <div class="dropdown">
<label tabindex="0" class="btn btn-ghost lg:hidden"> <label tabindex="0" class="btn btn-ghost lg:hidden">
@@ -60,27 +27,6 @@
<li v-if="!user"><a title="github" href="https://github.com/JavascriptMick/supanuxt-saas"><Icon name="mdi:github"/></a></li> <li v-if="!user"><a title="github" href="https://github.com/JavascriptMick/supanuxt-saas"><Icon name="mdi:github"/></a></li>
</ul> </ul>
</div> </div>
<div class="navbar-end" v-if="user"> <UserAccount v-if="user" :user="user"/>
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
<img v-if="user.user_metadata.avatar_url" :src="user.user_metadata.avatar_url" alt="avatar image"/>
<img v-else src="~/assets/images/avatar.svg" alt="default avatar image"/>
</div>
</label>
<ul tabindex="0" class="mt-3 p-2 shadow menu menu-compact dropdown-content bg-base-100 rounded-box w-52">
<li v-if="user">{{ user.email }}</li>
<li><NuxtLink to="/account">Account</NuxtLink></li>
<li><a href="#" @click.prevent="signout()">Signout</a></li>
<template v-if="dbUser?.memberships && dbUser?.memberships.length > 1">
<li>Switch Account</li>
<li v-for="membership in dbUser?.memberships">
<a v-if="membership.account_id !== activeAccountId && !membership.pending" href="#" @click="accountStore.changeActiveAccount(membership.account_id)">{{ membership.account.name }}</a>
<span v-if="membership.pending">{{ membership.account.name }} (pending)</span>
</li>
</template>
</ul>
</div>
</div>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import { NotificationType } from '#imports';
import { storeToRefs } from 'pinia';
const notifyStore = useNotifyStore();
const { notifications } = storeToRefs(notifyStore);
const classNameForType = (type: NotificationType) => {
switch (type) {
case NotificationType.Info:
return "alert alert-info";
case NotificationType.Success:
return "alert alert-success";
case NotificationType.Warning:
return "alert alert-warning";
case NotificationType.Error:
return "alert alert-error";
}
};
</script>
<template>
<div class="toast toast-end toast-top">
<div v-for="notification in notifications" :class="classNameForType(notification.type)" >
<div>
<button
@click.prevent="notifyStore.removeNotification(notification)"
type="button"
class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
aria-label="Close">
<span class="sr-only">Close</span>
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</button>
<span>&nbsp;{{notification.message}}</span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
const props = defineProps({
user: {
type: Object,
required: true,
},
});
const { user } = props;
</script>
<template>
<div class="navbar-end">
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
<img v-if="user.user_metadata?.avatar_url" :src="user.user_metadata.avatar_url" alt="avatar image"/>
<img v-else src="~/assets/images/avatar.svg" alt="default avatar image"/>
</div>
</label>
<ul tabindex="0" class="mt-3 p-2 shadow menu menu-compact dropdown-content bg-base-100 rounded-box w-52">
<li v-if="user">{{ user.email }}</li>
<li><NuxtLink to="/account">Account</NuxtLink></li>
<li><UserAccountSignout/></li>
<UserAccountSwitch/>
</ul>
</div>
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
const supabase = useSupabaseAuthClient();
const accountStore = useAccountStore();
onMounted(async () => {
await accountStore.init()
});
async function signout() {
await supabase.auth.signOut();
if(accountStore){
accountStore.signout();
}
navigateTo('/', {replace: true});
}
</script>
<template>
<a href="#" @click.prevent="signout()">Signout</a>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
const accountStore = useAccountStore()
const { dbUser, activeAccountId } = storeToRefs(accountStore);
onMounted(async () => {
await accountStore.init()
});
</script>
<template>
<template v-if="dbUser?.memberships && dbUser?.memberships.length > 1">
<li>Switch Account</li>
<li v-for="membership in dbUser?.memberships">
<a v-if="membership.account_id !== activeAccountId && !membership.pending" href="#" @click="accountStore.changeActiveAccount(membership.account_id)">{{ membership.account.name }}</a>
<span v-if="membership.pending">{{ membership.account.name }} (pending)</span>
</li>
</template>
</template>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "supanuxt-saas", "name": "supanuxt-saas",
"version": "1.4.0", "version": "1.4.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "supanuxt-saas", "name": "supanuxt-saas",
"version": "1.4.0", "version": "1.4.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "supanuxt-saas", "name": "supanuxt-saas",
"version": "1.4.0", "version": "1.4.1",
"author": { "author": {
"name": "Michael Dausmann", "name": "Michael Dausmann",
"email": "mdausmann@gmail.com", "email": "mdausmann@gmail.com",

View File

@@ -175,4 +175,5 @@ const user = useSupabaseUser()
</div> </div>
</div> </div>
</section> </section>
</div></template> </div>
</template>

View File

@@ -36,18 +36,19 @@
<h2>Security of your information</h2> <h2>Security of your information</h2>
<p>We take reasonable measures to protect your personal information from unauthorized access, use, or disclosure. <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 However, no data transmission over the internet or electronic storage is completely secure, so we cannot guarantee
the absolute security of your information.</p> the absolute security of your information.</p>
<h2>Changes to this privacy statement</h2> <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 <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> back periodically to review the most current version of the statement.</p>
<h2>Contact us</h2> <h2>Contact us</h2>
<p>If you have any questions or concerns about our privacy practices, please contact us at [insert contact <p>If you have any questions or concerns about our privacy practices, please contact us at [insert contact
information].</p> information].</p>
</div></template> </div>
</template>

View File

@@ -1,6 +1,6 @@
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client' import { createTRPCNuxtClient, httpBatchLink } from "trpc-nuxt/client";
import type { AppRouter } from '~/server/api/trpc/[trpc]' import type { AppRouter } from "~/server/api/trpc/[trpc]";
import superjson from 'superjson'; import superjson from "superjson";
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
/** /**
@@ -10,15 +10,15 @@ export default defineNuxtPlugin(() => {
const client = createTRPCNuxtClient<AppRouter>({ const client = createTRPCNuxtClient<AppRouter>({
links: [ links: [
httpBatchLink({ httpBatchLink({
url: '/api/trpc', url: "/api/trpc",
}), }),
], ],
transformer: superjson, transformer: superjson,
}) });
return { return {
provide: { provide: {
client, client,
}, },
} };
}) });

View File

@@ -11,10 +11,10 @@ export interface Notification{
} }
export enum NotificationType{ export enum NotificationType{
Info = "alert alert-info", Info,
Success = "alert alert-success", Success,
Warning = "alert alert-warning", Warning,
Error = "alert alert-error", Error,
} }
interface State { interface State {