From 78aa62ee4c448b00bef6795aea41981ede9534ce Mon Sep 17 00:00:00 2001
From: nnivxix
Date: Sun, 15 Dec 2024 15:25:09 +0700
Subject: [PATCH] wip: basic crud supabase and indexedDB for collection
---
.env.example | 2 +
.gitignore | 3 +
README.md | 30 +++++++++
package.json | 1 +
pnpm-lock.yaml | 97 ++++++++++++++++++++++++++++
src/App.vue | 10 ++-
src/composables/useCollection.js | 9 ++-
src/composables/useFormCollection.js | 6 +-
src/pages/collection/create.vue | 2 +-
src/pages/collection/edit.vue | 25 ++++---
src/pages/collection/id.vue | 53 +++++++++------
src/pages/index.vue | 3 +-
src/repositories/adapter.js | 28 +++++---
src/repositories/indexedDB.js | 6 +-
src/repositories/supabase.js | 72 +++++++++++++++++++++
src/types.d.ts | 2 +-
16 files changed, 297 insertions(+), 52 deletions(-)
create mode 100644 .env.example
create mode 100644 src/repositories/supabase.js
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..5a59748
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+VITE_SUPABASE_URL=
+VITE_SUPABASE_KEY=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index afdf4b6..1d9c3d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,6 @@ dev-dist
*.njsproj
*.sln
*.sw?
+
+
+.env
\ No newline at end of file
diff --git a/README.md b/README.md
index e6476ce..258d86e 100644
--- a/README.md
+++ b/README.md
@@ -27,3 +27,33 @@ Create todo right now, let's do great today, don't busy be productive.
Start at 09:31:41
Duration 418ms
```
+
+
+## Supabase Setup
+
+```sql
+create table
+ public.collections (
+ id bigint generated by default as identity not null,
+ uid character varying not null,
+ created_at timestamp with time zone not null default now(),
+ name character varying not null,
+ description text null,
+ constraint collections_pkey primary key (id),
+ constraint collections_uid_key unique (uid)
+ ) tablespace pg_default;
+```
+
+```sql
+create table
+ public.todos (
+ id bigint generated by default as identity not null,
+ name character varying null,
+ priority character varying null,
+ is_done boolean not null default false,
+ created_at timestamp with time zone not null default now(),
+ collection_id bigint not null,
+ constraint todos_pkey primary key (id),
+ constraint todos_collection_id_fkey foreign key (collection_id) references collections (id) on update cascade on delete cascade
+ ) tablespace pg_default;
+```
\ No newline at end of file
diff --git a/package.json b/package.json
index 8783ff1..092cdd1 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@highlightjs/vue-plugin": "^2.1.0",
+ "@supabase/supabase-js": "^2.46.1",
"highlight.js": "^11.10.0",
"idb": "^8.0.0",
"nanoid": "^5.0.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ae22963..fefa200 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
'@highlightjs/vue-plugin':
specifier: ^2.1.0
version: 2.1.0(highlight.js@11.10.0)(vue@3.4.20)
+ '@supabase/supabase-js':
+ specifier: ^2.46.1
+ version: 2.46.1
highlight.js:
specifier: ^11.10.0
version: 11.10.0
@@ -918,6 +921,28 @@ packages:
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
+ '@supabase/auth-js@2.65.1':
+ resolution: {integrity: sha512-IA7i2Xq2SWNCNMKxwmPlHafBQda0qtnFr8QnyyBr+KaSxoXXqEzFCnQ1dGTy6bsZjVBgXu++o3qrDypTspaAPw==}
+
+ '@supabase/functions-js@2.4.3':
+ resolution: {integrity: sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==}
+
+ '@supabase/node-fetch@2.6.15':
+ resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
+ engines: {node: 4.x || >=6.0.0}
+
+ '@supabase/postgrest-js@1.16.3':
+ resolution: {integrity: sha512-HI6dsbW68AKlOPofUjDTaosiDBCtW4XAm0D18pPwxoW3zKOE2Ru13Z69Wuys9fd6iTpfDViNco5sgrtnP0666A==}
+
+ '@supabase/realtime-js@2.10.7':
+ resolution: {integrity: sha512-OLI0hiSAqQSqRpGMTUwoIWo51eUivSYlaNBgxsXZE7PSoWh12wPRdVt0psUMaUzEonSB85K21wGc7W5jHnT6uA==}
+
+ '@supabase/storage-js@2.7.1':
+ resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==}
+
+ '@supabase/supabase-js@2.46.1':
+ resolution: {integrity: sha512-HiBpd8stf7M6+tlr+/82L8b2QmCjAD8ex9YdSAKU+whB/SHXXJdus1dGlqiH9Umy9ePUuxaYmVkGd9BcvBnNvg==}
+
'@surma/rollup-plugin-off-main-thread@2.2.3':
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
@@ -930,12 +955,18 @@ packages:
'@types/node@18.7.14':
resolution: {integrity: sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==}
+ '@types/phoenix@1.6.5':
+ resolution: {integrity: sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==}
+
'@types/resolve@1.17.1':
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
'@types/trusted-types@2.0.2':
resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==}
+ '@types/ws@8.5.13':
+ resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==}
+
'@vitejs/plugin-vue@5.0.4':
resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -2146,6 +2177,9 @@ packages:
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
engines: {node: '>=6'}
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
@@ -2313,6 +2347,9 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
webidl-conversions@4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
@@ -2332,6 +2369,9 @@ packages:
resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==}
engines: {node: '>=18'}
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
whatwg-url@7.1.0:
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
@@ -3365,6 +3405,48 @@ snapshots:
'@sinclair/typebox@0.27.8': {}
+ '@supabase/auth-js@2.65.1':
+ dependencies:
+ '@supabase/node-fetch': 2.6.15
+
+ '@supabase/functions-js@2.4.3':
+ dependencies:
+ '@supabase/node-fetch': 2.6.15
+
+ '@supabase/node-fetch@2.6.15':
+ dependencies:
+ whatwg-url: 5.0.0
+
+ '@supabase/postgrest-js@1.16.3':
+ dependencies:
+ '@supabase/node-fetch': 2.6.15
+
+ '@supabase/realtime-js@2.10.7':
+ dependencies:
+ '@supabase/node-fetch': 2.6.15
+ '@types/phoenix': 1.6.5
+ '@types/ws': 8.5.13
+ ws: 8.16.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ '@supabase/storage-js@2.7.1':
+ dependencies:
+ '@supabase/node-fetch': 2.6.15
+
+ '@supabase/supabase-js@2.46.1':
+ dependencies:
+ '@supabase/auth-js': 2.65.1
+ '@supabase/functions-js': 2.4.3
+ '@supabase/node-fetch': 2.6.15
+ '@supabase/postgrest-js': 1.16.3
+ '@supabase/realtime-js': 2.10.7
+ '@supabase/storage-js': 2.7.1
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
'@surma/rollup-plugin-off-main-thread@2.2.3':
dependencies:
ejs: 3.1.8
@@ -3378,12 +3460,18 @@ snapshots:
'@types/node@18.7.14': {}
+ '@types/phoenix@1.6.5': {}
+
'@types/resolve@1.17.1':
dependencies:
'@types/node': 18.7.14
'@types/trusted-types@2.0.2': {}
+ '@types/ws@8.5.13':
+ dependencies:
+ '@types/node': 18.7.14
+
'@vitejs/plugin-vue@5.0.4(vite@5.1.4(@types/node@18.7.14)(terser@5.15.0))(vue@3.4.20)':
dependencies:
vite: 5.1.4(@types/node@18.7.14)(terser@5.15.0)
@@ -4660,6 +4748,8 @@ snapshots:
universalify: 0.2.0
url-parse: 1.5.10
+ tr46@0.0.3: {}
+
tr46@1.0.1:
dependencies:
punycode: 2.3.1
@@ -4817,6 +4907,8 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
+ webidl-conversions@3.0.1: {}
+
webidl-conversions@4.0.2: {}
webidl-conversions@7.0.0: {}
@@ -4832,6 +4924,11 @@ snapshots:
tr46: 5.0.0
webidl-conversions: 7.0.0
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
whatwg-url@7.1.0:
dependencies:
lodash.sortby: 4.7.0
diff --git a/src/App.vue b/src/App.vue
index d4f4035..554d09d 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,15 +1,21 @@
-
+
+
+
diff --git a/src/composables/useCollection.js b/src/composables/useCollection.js
index 104c629..5565246 100644
--- a/src/composables/useCollection.js
+++ b/src/composables/useCollection.js
@@ -31,7 +31,7 @@ const useCollection = () => {
const addCollection = (collection) => {
collections.value.push(collection);
- dbCollection.add(collection);
+ store.create(collection);
};
const getCollections = async () => {
collections.value = await store.all();
@@ -41,8 +41,11 @@ const useCollection = () => {
* @param {string} id
* @returns {Collection}
*/
- const getDetailCollection = (id) => {
- return collections.value.find((collection) => collection.id == id);
+ const getDetailCollection = async (id) => {
+ const collection = (await model()).find(id);
+
+ return collection;
+ // return collections.value.find((collection) => collection.id == id);
};
/** @param {string} id */
diff --git a/src/composables/useFormCollection.js b/src/composables/useFormCollection.js
index 8218756..64eba7a 100644
--- a/src/composables/useFormCollection.js
+++ b/src/composables/useFormCollection.js
@@ -33,7 +33,7 @@ const useFormCollection = () => {
async function editCurrentCollection() {
form.value.id = collection.value.id;
const updatedCollection = updateCollection(form.value);
- console.log(updatedCollection);
+
return updatedCollection;
}
function addNewCollection() {
@@ -44,9 +44,11 @@ const useFormCollection = () => {
id: nanoid(),
name: form.value.name,
description: form.value.description,
- todos: [],
+ // todos: [],
};
+ // Validation if collection more than 10 just throw error
+
addCollection(collection);
resetForm();
return collection;
diff --git a/src/pages/collection/create.vue b/src/pages/collection/create.vue
index 58b0d49..da3283e 100644
--- a/src/pages/collection/create.vue
+++ b/src/pages/collection/create.vue
@@ -15,7 +15,7 @@ const schema = yup.object({
/** @param {import('@/types').Collection} values */
const onSubmit = (values) => {
form.value = values;
- form.value.todos = [];
+ // form.value.todos = [];
const collection = addNewCollection();
router.push(`/collection/${collection.id}`);
diff --git a/src/pages/collection/edit.vue b/src/pages/collection/edit.vue
index 5132ab4..e090116 100644
--- a/src/pages/collection/edit.vue
+++ b/src/pages/collection/edit.vue
@@ -4,16 +4,18 @@ import { onMounted, toRaw } from "vue";
import { Form } from "vee-validate";
import * as yup from "yup";
import useFormCollection from "@/composables/useFormCollection";
-import dbCollection from "@/repositories/db-collection";
+import model from "@/repositories/adapter";
const router = useRouter();
const route = useRoute();
-const { editCurrentCollection, form, collection } = useFormCollection();
+const { form, collection } = useFormCollection();
+
const schema = yup.object({
name: yup.string().required(),
description: yup.string(),
});
const isEdit = route.fullPath.includes("edit");
+const store = await model();
/** @param {import('@/types').Collection} values */
const onSubmit = async (values) => {
@@ -21,20 +23,23 @@ const onSubmit = async (values) => {
...values,
todos: toRaw(collection.value.todos),
};
- const updatedCollection = await editCurrentCollection();
- router.push(`/collection/${updatedCollection.id}`);
+ await store.update(route.params.id, {
+ name: values.name,
+ description: values.description,
+ });
+
+ router.push(`/collection/${route.params.id}`);
};
onMounted(async () => {
+ collection.value = await store.find(route.params.id);
if (isEdit) {
- const dataCollection = await dbCollection.show(route.params.id);
- collection.value = dataCollection;
- if (!!dataCollection) {
+ if (!!collection.value) {
form.value = {
- name: dataCollection.name,
- description: dataCollection.description,
- todos: dataCollection.todos,
+ name: collection.value.name,
+ description: collection.value.description,
+ todos: collection.value.todos,
};
return;
}
diff --git a/src/pages/collection/id.vue b/src/pages/collection/id.vue
index bb44ad2..9c2e817 100644
--- a/src/pages/collection/id.vue
+++ b/src/pages/collection/id.vue
@@ -2,6 +2,8 @@
import { useRoute, useRouter } from "vue-router";
import { ref, toRaw, onMounted } from "vue";
import { useForm } from "vee-validate";
+import model from "@/repositories/adapter";
+
import * as yup from "yup";
import useCollection from "@/composables/useCollection";
@@ -10,9 +12,13 @@ import useFormTodo from "@/composables/useFormTodo";
import dbCollections from "@/repositories/db-collection";
import exportCollection from "@/utils/export-collection";
+const store = await model();
const route = useRoute();
const router = useRouter();
-const { deleteColllection, collection } = useCollection();
+const { collection, collections } = useCollection();
+
+collection.value = await store.find(route.params.id);
+
const { addTodo, markTodo, editTodo, deleteTodo, doneTodos, todos } = useTodo();
const { formTodo, isEditing, resetForm: resetFormTodo } = useFormTodo();
const vFormTodo = useForm({
@@ -102,10 +108,16 @@ const handleMarkTodo = (index) => {
};
/** @param {string} id */
-const handleDeleteCollection = (id) => {
+const handleDeleteCollection = async (id) => {
const question = confirm("Are you sure delete this collection?");
if (question) {
- deleteColllection(id);
+ store.delete(id);
+
+ const index = collections.value.findIndex((coll) => coll.id === id);
+ if (index > 0) {
+ collections.value.splice(index, 1);
+ }
+
router.push("/");
return;
}
@@ -113,7 +125,6 @@ const handleDeleteCollection = (id) => {
};
onMounted(async () => {
- collection.value = await dbCollections.show(route.params.id);
resetFormTodo();
vFormTodo.setValues({
...formTodo.value,
@@ -135,14 +146,14 @@ onMounted(async () => {
{{ collection.description }}
no description
-
-
- You have {{ todos?.length }} / {{ doneTodos?.length }}
- {{ todos.length > 1 ? "todos" : "todo" }}
-
+
+
@@ -172,15 +183,15 @@ onMounted(async () => {
-
+
- {{ collection.name }} ({{ collection.todos.length }})
+ {{ collection.name }}
+
{
let store = null;
+
if (connection === "indexedDB") {
store = indexedDB;
}
+ if (connection === "supabase") {
+ store = supabase;
+ }
// Check if store is set
if (!store) {
@@ -15,17 +25,17 @@ const model = async (connection = "indexedDB") => {
all: async () => {
return await store.index();
},
- find: (id) => {
- return store.find(id);
+ find: (key) => {
+ return store.find(key);
},
- create: (collection) => {
- store.create(collection);
+ create: (values) => {
+ store.create(values);
},
- update: async (id, collection) => {
- await store.update(collection);
+ update: async (key, values) => {
+ await store.update(key, values);
},
- delete: (id) => {
- store.delete(id);
+ delete: (key) => {
+ store.delete(key);
},
};
};
diff --git a/src/repositories/indexedDB.js b/src/repositories/indexedDB.js
index bc20f81..0966f3d 100644
--- a/src/repositories/indexedDB.js
+++ b/src/repositories/indexedDB.js
@@ -26,8 +26,10 @@ const indexedDB = {
* @param {Collection} collection
* @returns {Promise}
*/
- async update(collection) {
- return (await dbPromise).put("collections", collection);
+ async update(key, collection) {
+ const values = { id: key, ...collection };
+ const db = await dbPromise;
+ db.put("collections", values);
},
/**
diff --git a/src/repositories/supabase.js b/src/repositories/supabase.js
new file mode 100644
index 0000000..42e55e8
--- /dev/null
+++ b/src/repositories/supabase.js
@@ -0,0 +1,72 @@
+import { createClient } from "@supabase/supabase-js";
+
+const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+const supabaseKey = import.meta.env.VITE_SUPABASE_KEY;
+
+const client = createClient(supabaseUrl, supabaseKey);
+
+/**
+ * @typedef {import('@/types').Collection} Collection
+ */
+
+const supabase = {
+ async index() {
+ // const { data } = await client.from("collections").select(`*, todos(count)`);
+ const { data } = await client
+ .from("collections")
+ .select(`name, description, id:uid, created_at, todos(*)`);
+
+ console.log(data);
+ return data;
+ },
+
+ /** @param {Collection} collection */
+ async create(collection) {
+ const { data } = await client
+ .from("collections")
+ .insert({
+ uid: collection.id,
+ name: collection.name,
+ description: collection.description,
+ })
+ .select(`name, description, id:uid, created_at, todos(*)`);
+
+ return data;
+ },
+
+ async find(id) {
+ // return (await dbPromise).get("collections", id);
+ const { data } = await client
+ .from("collections")
+ .select(`name, description, id:uid, created_at, todos(*)`)
+ .eq("uid", id)
+ .limit(1);
+
+ return data.at(0);
+ },
+
+ async update(id, values) {
+ const { data, error } = await client
+ .from("collections")
+ .update({ ...values })
+ .eq("uid", id)
+ .select();
+
+ if (error) {
+ console.error(error);
+ }
+ return data;
+ },
+ async delete(key) {
+ const { data, error } = await client
+ .from("collections")
+ .delete()
+ .eq("uid", key);
+
+ if (error) {
+ console.error(error);
+ }
+ return data;
+ },
+};
+export default supabase;
diff --git a/src/types.d.ts b/src/types.d.ts
index d6ed786..9c2ab5e 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -1,4 +1,4 @@
-export interface Collection {
+nexport interface Collection {
id: string;
name: string;
description: string;