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
## 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
- Cookie Consent
```npm i vanilla-cookieconsent```
`npm i vanilla-cookieconsent`
## Version 1.3.0
- Add an example of usage limits (Notes AI Gen).
- Includes non-destructive schema changes
```npx prisma db push```
`npx prisma db push`
## 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.
## 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)
```
npm install @prisma/client@5
npm install -D prisma@5
@@ -21,17 +32,21 @@ npx prisma generate
```
- Upgrade Nuxt to 3.7.0
```
npx nuxi upgrade --force
```
## 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)
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
I Followed instructions from here https://nuxt.com/docs/getting-started/installation
```bash
@@ -42,6 +57,7 @@ code nuxt3-boilerplate/
npm install
npm run dev -- -o
```
### Setup Supabase
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
```
modules: ['@nuxtjs/supabase']
```
### Setup Google OAuth
Follow these instructions to add google oath https://supabase.com/docs/guides/auth/social-login/auth-google
### 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
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.
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
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.
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
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
- 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/)
## 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
Discord [here](https://discord.gg/3hWPDTA4kD)

View File

@@ -1,43 +1,10 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
const supabase = useSupabaseAuthClient();
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>
<template>
<div class="navbar bg-base-100">
<div class="toast toast-end toast-top">
<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>
<Notifications/>
<div class="navbar-start">
<div class="dropdown">
<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>
</ul>
</div>
<div class="navbar-end" v-if="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>
<UserAccount v-if="user" :user="user"/>
</div>
</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",
"version": "1.4.0",
"version": "1.4.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "supanuxt-saas",
"version": "1.4.0",
"version": "1.4.1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

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

View File

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

View File

@@ -36,18 +36,19 @@
<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>
<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>
<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>
</div>
</template>

View File

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

View File

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