├── supabase ├── .gitignore ├── functions │ └── .vscode │ │ └── settings.json ├── migrations │ └── 20230717154337_initial_schema.sql ├── config.toml ├── schema.ts └── seed.sql ├── .npmrc ├── server └── tsconfig.json ├── app.config.ts ├── assets └── css │ └── main.css ├── public └── favicon.ico ├── .env.example ├── .fund-profile.json ├── pages ├── index.vue ├── logout.vue ├── projects │ ├── [uuid].vue │ └── create.vue ├── categories │ └── [uuid].vue ├── profile.vue ├── login.vue └── register.vue ├── tsconfig.json ├── composables ├── useTypedSupabaseClient.ts ├── usePaginator.ts ├── useCategories.ts ├── useAlerts.ts ├── useKdaUsd.ts ├── useProjects.ts ├── useWallet.ts └── usePact.ts ├── .deploy-data.json ├── middleware └── auth.ts ├── crowdfund.json ├── .deploy-profile.json ├── .gitignore ├── app.vue ├── components ├── Date.vue ├── TimeAgo.vue ├── Balance.client.vue ├── ProjectsList.vue ├── ProjectPledgeForm.vue ├── Pagination.vue ├── AppFileUpload.vue ├── AppAlerts.vue ├── Money.vue ├── FormField.vue ├── ProjectCard.vue ├── NavBar.vue ├── ProjectsDetails.vue └── ProjectsForm.vue ├── nuxt.config.ts ├── tailwind.config.js ├── types └── index.ts ├── utils └── index.ts ├── README.md ├── package.json └── crowdfund.pact /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | appName: "✨ DecentralSpark", 3 | }); 4 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueschool/vue-forge-episode-4/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SUPABASE_URL="http://localhost:3000" 2 | SUPABASE_KEY="" 3 | SUPABASE_SERVICE_KEY="" -------------------------------------------------------------------------------- /.fund-profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "task": ["fund"], 3 | "network": "fast-development", 4 | "chainId": "0", 5 | "endpoint": "http://devnet:8080" 6 | } 7 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /supabase/functions/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "editor.defaultFormatter": "denoland.vscode-deno" 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { "types": [".kadena/pactjs-generated"] } 5 | } 6 | -------------------------------------------------------------------------------- /composables/useTypedSupabaseClient.ts: -------------------------------------------------------------------------------- 1 | import type { Database } from "@/supabase/schema"; 2 | 3 | export const useTypedSupabaseClient = () => { 4 | return useSupabaseClient(); 5 | }; 6 | -------------------------------------------------------------------------------- /pages/logout.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /.deploy-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ns":"free", 3 | "admin-keyset":{ 4 | "keys":["368820f80c324bbc7c2b0610688a7da43e39f91d118732671cd9c7500ff43cca"], 5 | "pred":"keys-all" 6 | }, 7 | "upgrade":false 8 | } 9 | -------------------------------------------------------------------------------- /middleware/auth.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware((to, _from) => { 2 | const user = useSupabaseUser(); 3 | 4 | if (!user.value) { 5 | return navigateTo(`/login?redirect=${to.fullPath}`); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /crowdfund.json: -------------------------------------------------------------------------------- 1 | { 2 | "ns": "free", 3 | "upgrade": false, 4 | "admin-keyset": { 5 | "keys": [ 6 | "368820f80c324bbc7c2b0610688a7da43e39f91d118732671cd9c7500ff43cca" 7 | ], 8 | "pred": "keys-all" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.deploy-profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "task": ["deploy"], 3 | "pactFile": "crowdfund.pact", 4 | "dataFile": ".deploy-data.json", 5 | "deployTargets": ["l1"], 6 | "l1Endpoint": "http://devnet:8080", 7 | "l1Chains": ["0"], 8 | "signer": "sender00" 9 | } 10 | -------------------------------------------------------------------------------- /pages/projects/[uuid].vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /pages/projects/create.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .nuxt 4 | .nitro 5 | .cache 6 | dist 7 | 8 | .kda-history.json 9 | 10 | # Node dependencies 11 | node_modules 12 | 13 | # Logs 14 | logs 15 | *.log 16 | 17 | # Misc 18 | .DS_Store 19 | .fleet 20 | .idea 21 | 22 | # Local env files 23 | .env 24 | .env.* 25 | !.env.example 26 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /components/Date.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /components/TimeAgo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | devtools: { enabled: true }, 4 | ssr: false, 5 | modules: [ 6 | "@nuxtjs/tailwindcss", 7 | "@nuxtjs/supabase", 8 | "@vueuse/nuxt", 9 | "@vee-validate/nuxt", 10 | "nuxt-proxy", 11 | ], 12 | proxy: { 13 | options: { 14 | target: "http://localhost:55321", 15 | changeOrigin: true, 16 | pathFilter: ["/rest/**", "/auth/**", "/storage/**"], 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./components/**/*.{js,vue,ts}", 5 | "./layouts/**/*.vue", 6 | "./pages/**/*.vue", 7 | "./plugins/**/*.{js,ts}", 8 | "./nuxt.config.{js,ts}", 9 | "./app.vue", 10 | ], 11 | theme: { 12 | extend: {}, 13 | }, 14 | plugins: [require("daisyui")], 15 | daisyui: { 16 | theme: "cupcake", 17 | darkMode: false, 18 | themes: ["cupcake"], 19 | }, 20 | safelist: [ 21 | "border-t-warning", 22 | "border-t-error", 23 | "border-t-success", 24 | "border-t-info", 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Database } from "@/supabase/schema"; 2 | export type UuidT = string; 3 | 4 | export type CategoryT = Database["public"]["Tables"]["categories"]["Row"]; 5 | 6 | export type ProjectT = Database["public"]["Tables"]["projects"]["Row"] & { 7 | category?: CategoryT; 8 | }; 9 | 10 | export type PaginationT = { 11 | total: number; 12 | page: number; 13 | limit: number; 14 | pages: number; 15 | isNextAvailable: boolean; 16 | isPrevAvailable: boolean; 17 | next: number | null; 18 | prev: number | null; 19 | }; 20 | 21 | export type ApiResponseT = { 22 | data: any; 23 | pagination: PaginationT; 24 | }; 25 | -------------------------------------------------------------------------------- /pages/categories/[uuid].vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /components/Balance.client.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 23 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export function getDateXMonthsFromNow(months: number) { 2 | return new Date(new Date().setMonth(new Date().getMonth() + months)); 3 | } 4 | 5 | export function getDateXDaysFromNow(days: number) { 6 | return new Date(new Date().setDate(new Date().getDate() + days)); 7 | } 8 | 9 | export function getDateXMinutesFromNow(minutes: number) { 10 | return new Date(new Date().setMinutes(new Date().getMinutes() + minutes)); 11 | } 12 | 13 | export function getExactStartTimeFromDateField(startsAt: string) { 14 | const selectedDate = new Date(`${startsAt} 00:00:00`); 15 | const isToday = selectedDate.getDate() === new Date().getDate(); 16 | 17 | return isToday 18 | ? new Date( 19 | new Date().setMinutes(new Date().getMinutes() + 20) 20 | ).toISOString() 21 | : new Date(startsAt).toISOString(); 22 | } 23 | -------------------------------------------------------------------------------- /components/ProjectsList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | -------------------------------------------------------------------------------- /components/ProjectPledgeForm.vue: -------------------------------------------------------------------------------- 1 | 11 | 28 | -------------------------------------------------------------------------------- /composables/usePaginator.ts: -------------------------------------------------------------------------------- 1 | export const usePaginator = ({ 2 | limit = 8, 3 | page = 1, 4 | }: { 5 | limit?: number; 6 | page?: number; 7 | } = {}) => { 8 | function getRangeStart() { 9 | return (page - 1) * limit; 10 | } 11 | 12 | function getRangeEnd() { 13 | return page * limit - 1; 14 | } 15 | 16 | function getPaginationObject(total: number) { 17 | const totalPages = Math.ceil(total / limit); 18 | const isNextAvailable = totalPages > page; 19 | const isPrevAvailable = page !== 1; 20 | 21 | return { 22 | total: total, 23 | page, 24 | limit, 25 | pages: totalPages, 26 | isNextAvailable, 27 | isPrevAvailable, 28 | next: isNextAvailable ? page + 1 : null, 29 | prev: isPrevAvailable ? page - 1 : null, 30 | }; 31 | } 32 | 33 | return { getRangeStart, getRangeEnd, getPaginationObject }; 34 | }; 35 | -------------------------------------------------------------------------------- /components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 56 | -------------------------------------------------------------------------------- /components/AppFileUpload.vue: -------------------------------------------------------------------------------- 1 | 38 | 54 | -------------------------------------------------------------------------------- /composables/useCategories.ts: -------------------------------------------------------------------------------- 1 | import { PaginationT, CategoryT, UuidT, ProjectT } from "~/types"; 2 | 3 | export const useCategories = () => { 4 | const supabase = useTypedSupabaseClient(); 5 | const category = ref< 6 | { 7 | projects: ProjectT[]; 8 | } & CategoryT 9 | >(); 10 | const categories = ref([]); 11 | const pagination = ref(); 12 | 13 | const fetchAll = async ({ page }: { page: number } = { page: 1 }) => { 14 | const { getPaginationObject, getRangeEnd, getRangeStart } = usePaginator({ 15 | limit: 10, 16 | page, 17 | }); 18 | 19 | let { data, error, count } = await supabase 20 | .from("categories") 21 | .select("*", { count: "exact" }) 22 | .range(getRangeStart(), getRangeEnd()); 23 | 24 | if (error || !data) { 25 | throw new Error(error?.message || "Error fetching projects"); 26 | } 27 | 28 | categories.value = data; 29 | 30 | if (!count) throw new Error("Count not fetched from Supabase"); 31 | pagination.value = getPaginationObject(count); 32 | }; 33 | 34 | const fetchOne = async ({ uuid }: { uuid: UuidT }) => { 35 | const { data, error } = await supabase 36 | .from("categories") 37 | .select("*, projects(*)") 38 | .eq("uuid", uuid) 39 | .single(); 40 | if (error || !data) 41 | throw new Error(error?.message || "Error fetching category"); 42 | category.value = data; 43 | }; 44 | 45 | return { 46 | item: category, 47 | list: categories, 48 | pagination, 49 | 50 | fetchAll, 51 | fetchOne, 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /components/AppAlerts.vue: -------------------------------------------------------------------------------- 1 | 4 | 34 | 35 | 58 | -------------------------------------------------------------------------------- /pages/profile.vue: -------------------------------------------------------------------------------- 1 | 18 | 57 | -------------------------------------------------------------------------------- /composables/useAlerts.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid"; 2 | interface AlertOptions { 3 | type?: "success" | "error" | "info" | "warning"; 4 | title?: string; 5 | dismissiable?: boolean; 6 | timeout?: number; 7 | } 8 | interface Alert extends AlertOptions { 9 | message: string; 10 | id: string; 11 | } 12 | 13 | export const useAlerts = () => { 14 | const alerts = useState("appAlerts", () => []); 15 | function alert(message: string, options: AlertOptions) { 16 | const id = nanoid(); 17 | const defaults: Partial = { 18 | type: "info", 19 | dismissiable: true, 20 | timeout: 5000, 21 | }; 22 | alerts.value.push({ id, ...defaults, message, ...options }); 23 | 24 | let timeout = 25 | options.timeout === undefined ? defaults.timeout : options.timeout; 26 | if (timeout) { 27 | setTimeout(() => dismiss(id), timeout); 28 | } 29 | } 30 | 31 | function dismiss(idOrAlert: string | Alert) { 32 | const id = typeof idOrAlert === "string" ? idOrAlert : idOrAlert.id; 33 | alerts.value = alerts.value.filter((alert) => alert.id !== id); 34 | } 35 | 36 | function success(message: string, options: AlertOptions = {}) { 37 | alert(message, { ...options, type: "success" }); 38 | } 39 | 40 | function error(message: string, options: AlertOptions = {}) { 41 | alert(message, { ...options, type: "error" }); 42 | } 43 | 44 | function info(message: string, options: AlertOptions = {}) { 45 | alert(message, { ...options, type: "info" }); 46 | } 47 | 48 | function warning(message: string, options: AlertOptions = {}) { 49 | alert(message, { ...options, type: "warning" }); 50 | } 51 | 52 | return { 53 | success, 54 | info, 55 | warning, 56 | error, 57 | alerts, 58 | dismiss, 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to Get Started 2 | 3 | 1. Ensure you have [docker installed](https://docs.docker.com/get-docker/) and running 4 | 2. Clone the repo or [![Run with VS Code](https://badgen.net/badge/Run%20with%20/VS%20Code/5B3ADF?icon=https://runme.dev/img/logo.svg)](https://runme.dev/api/runme?repository=https%3A%2F%2Fgithub.com%2Fvueschool%2Fforge-4-poc.git&fileToOpen=README.md) 5 | 6 | ```sh 7 | git clone git@github.com:vueschool/vue-forge-episode-4.git 8 | ``` 9 | 10 | 3. Start on the boilerplate branch 11 | 12 | ``` 13 | git checkout boilerplate 14 | ``` 15 | 16 | 4. Install the dependencies 17 | 18 | ```sh 19 | yarn 20 | # or 21 | npm install 22 | ``` 23 | 24 | 5. Start the Supabase service 25 | 26 | ```sh 27 | yarn supabase:start 28 | # or 29 | npm run supabase:start 30 | ``` 31 | 32 | 6. The needed supabase environment variables will print after the service has started. Duplicate .env.example to .env and provide the following variables from the terminal print out. 33 | 34 | ```sh 35 | # this can stay the same 36 | SUPABASE_URL="http://localhost:3000" 37 | # anon key the terminal print out 38 | SUPABASE_KEY="" 39 | # service role key from the terminal print out 40 | SUPABASE_SERVICE_KEY="" 41 | ``` 42 | 43 | You can also retrieve these at any time by running the following: 44 | 45 | ```sh 46 | npx supabase status 47 | ``` 48 | 49 | 7. Migrate and seed your database with initial schema and values by running: 50 | 51 | ```sh 52 | yarn db:reset 53 | # or 54 | npm run db:reset 55 | ``` 56 | 57 | 8. Start the dev server 58 | 59 | ```sh 60 | yarn dev 61 | # or 62 | npm run dev 63 | ``` 64 | 65 | 9. [Follow these directions in the Devnet Setup Guide](https://vueschool.notion.site/DevNet-Setup-2ee973bf5061497d998823dd5cf43e6b?pvs=4) to get a local development blockchain network running. 66 | 67 | 10. That's it! 🎉 You're ready to go. 68 | -------------------------------------------------------------------------------- /composables/useKdaUsd.ts: -------------------------------------------------------------------------------- 1 | import { PactNumber } from "@kadena/pactjs"; 2 | 3 | export const useKdaUsd = ( 4 | usdOrKda: number | string | Ref, 5 | currency = "kda" 6 | ) => { 7 | const currentRate = useState("kda-usd-rate"); 8 | const rateError = useState("kda-usd-rate-error"); 9 | const rateLoading = useState("kda-usd-rate-loading"); 10 | const value = ref(usdOrKda); 11 | 12 | /** 13 | * Fetch rate from coingecko 14 | */ 15 | async function refreshRate() { 16 | rateLoading.value = true; 17 | rateError.value = false; 18 | currentRate.value = undefined; 19 | 20 | try { 21 | const data = await $fetch( 22 | "https://api.coingecko.com/api/v3/coins/kadena?tickers=true" 23 | ); 24 | const rate = data?.market_data?.current_price?.usd; 25 | if (rate) { 26 | currentRate.value = rate; 27 | } else { 28 | rateError.value = new Error("No rate found"); 29 | } 30 | } catch (error) { 31 | rateError.value = error; 32 | } finally { 33 | rateLoading.value = false; 34 | } 35 | } 36 | 37 | const asKda = computed(() => { 38 | if (currency === "kda") return value.value; 39 | return usdToKda(value.value); 40 | }); 41 | 42 | const asUsd = computed(() => { 43 | if (currency === "usd") return value.value; 44 | return kdaToUsd(value.value); 45 | }); 46 | 47 | /** 48 | * Convert USD to KDA 49 | */ 50 | function usdToKda(usd: number | string) { 51 | if (currentRate.value) { 52 | return new PactNumber(usd).dividedBy(currentRate.value); 53 | } 54 | return null; 55 | } 56 | 57 | /** 58 | * Convert KDA to USD 59 | */ 60 | function kdaToUsd(kda: number | string) { 61 | if (currentRate.value) { 62 | return new PactNumber(kda).multipliedBy(currentRate.value); 63 | } 64 | return null; 65 | } 66 | 67 | /** 68 | * If the rate not in memory, fetch it from coingecko 69 | */ 70 | async function initRate() { 71 | if (currentRate.value || rateLoading.value) return; 72 | await refreshRate(); 73 | } 74 | initRate(); 75 | 76 | return { 77 | asKda, 78 | asUsd, 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "scripts": { 5 | "build": "nuxt build", 6 | "supabase:start": "npx supabase start", 7 | "supabase:stop": "npx supabase stop", 8 | "dev": "nuxt dev", 9 | "generate": "nuxt generate", 10 | "preview": "nuxt preview", 11 | "postinstall": "nuxt prepare", 12 | "db:reset": "npx supabase db reset && npm run db:types", 13 | "db:types": "supabase gen types typescript --local > ./supabase/schema.ts", 14 | "kadena:devnet": "docker run -it -p 8080:1337 -v $HOME/.devnet/l1:/root/.devenv enof/devnet", 15 | "kadena:cli": "docker run --rm -it -v $(pwd):/app/ --add-host=devnet:host-gateway enof/kda-cli", 16 | "kadena:types": "npx pactjs contract-generate --contract=free.crowdfund --api=https://api.testnet.chainweb.com/chainweb/0.0/testnet04/chain/0/pact && npx pactjs contract-generate --contract=free.crowdfund --api=https://api.testnet.chainweb.com/chainweb/0.0/testnet04/chain/0/pact", 17 | "kadena:fund": "npm run kadena:cli -- --profile=.fund-profile.json", 18 | "kadena:deploy": "npm run kadena:cli -- --profile=.deploy-profile.json" 19 | }, 20 | "devDependencies": { 21 | "@faker-js/faker": "^8.0.2", 22 | "@nuxt/devtools": "latest", 23 | "@nuxtjs/supabase": "^0.3.8", 24 | "@types/node": "^18", 25 | "nuxt": "^3.6.3", 26 | "supabase": "^1.77.9" 27 | }, 28 | "dependencies": { 29 | "@kadena/client": "1.0.0-alpha.8", 30 | "@kadena/pactjs": "^0.3.0", 31 | "@kadena/pactjs-cli": "1.0.0-alpha.7", 32 | "@nuxtjs/tailwindcss": "^6.8.0", 33 | "@vee-validate/nuxt": "^4.10.7", 34 | "@vee-validate/zod": "^4.10.7", 35 | "@vueuse/core": "^10.2.1", 36 | "@vueuse/nuxt": "^10.2.1", 37 | "@walletconnect/encoding": "^1.0.2", 38 | "@walletconnect/modal": "^2.5.9", 39 | "@walletconnect/sign-client": "2.8.6", 40 | "@walletconnect/types": "2.8.6", 41 | "autoprefixer": "^10.4.14", 42 | "daisyui": "^3.1.7", 43 | "dotenv": "^16.3.1", 44 | "lokijs": "^1.5.12", 45 | "nanoid": "^4.0.2", 46 | "nuxt-proxy": "^0.4.1", 47 | "postcss": "^8.4.24", 48 | "tailwindcss": "^3.3.2", 49 | "vee-validate": "^4.10.7", 50 | "viem": "^1.3.0", 51 | "zod": "^3.21.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /components/Money.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 77 | -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 28 | 74 | 75 | 80 | -------------------------------------------------------------------------------- /components/FormField.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 48 | 88 | -------------------------------------------------------------------------------- /components/ProjectCard.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 78 | -------------------------------------------------------------------------------- /components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 73 | -------------------------------------------------------------------------------- /composables/useProjects.ts: -------------------------------------------------------------------------------- 1 | import { PaginationT, ProjectT, UuidT } from "~/types"; 2 | 3 | export const useProjects = () => { 4 | const supabase = useTypedSupabaseClient(); 5 | const project = ref({}); 6 | const projects = ref([]); 7 | const pagination = ref({}); 8 | 9 | const fetchAll = async ({ page }: { page: number } = { page: 1 }) => { 10 | const { getPaginationObject, getRangeEnd, getRangeStart } = usePaginator({ 11 | page, 12 | }); 13 | 14 | let { data, error, count } = await supabase 15 | .from("projects") 16 | .select("*, categories(*)", { count: "exact" }) 17 | .range(getRangeStart(), getRangeEnd()); 18 | 19 | if (error || !data) { 20 | throw new Error(error?.message || "Error fetching projects"); 21 | } 22 | 23 | projects.value = data.map((p) => { 24 | return { 25 | ...p, 26 | category: p.categories || undefined, 27 | }; 28 | }); 29 | 30 | if (!count) throw new Error("Count not fetched from Supabase"); 31 | pagination.value = getPaginationObject(count); 32 | }; 33 | 34 | const fetchOne = async ({ uuid }: { uuid: UuidT }) => { 35 | const { data, error } = await supabase 36 | .from("projects") 37 | .select("*") 38 | .eq("uuid", uuid) 39 | .single(); 40 | if (error || !data) { 41 | throw new Error(error?.message || "Error fetching project"); 42 | } 43 | project.value = data; 44 | }; 45 | 46 | const create = async (project: Partial): Promise => { 47 | const { data: newProject, error } = await supabase 48 | .from("projects") 49 | .insert(project as ProjectT) 50 | .select("*") 51 | .single(); 52 | 53 | if (error || !newProject) 54 | throw new Error(error?.message || "Error creating project"); 55 | 56 | return newProject; 57 | }; 58 | 59 | const updateStatusForRequestKey = async ( 60 | requestKey: string, 61 | data: Partial 62 | ): Promise => { 63 | const { data: project, error } = await supabase 64 | .from("projects") 65 | .update(data as ProjectT) 66 | .eq('requestKey' , requestKey as string ) 67 | .single() 68 | 69 | if (error) { 70 | throw new Error(error?.message || "Error updating project"); 71 | } 72 | 73 | return project; 74 | }; 75 | 76 | return { 77 | item: project, 78 | list: projects, 79 | pagination, 80 | 81 | create, 82 | updateStatusForRequestKey, 83 | fetchAll, 84 | fetchOne, 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /supabase/migrations/20230717154337_initial_schema.sql: -------------------------------------------------------------------------------- 1 | create table "public"."categories" ( 2 | "uuid" uuid not null default gen_random_uuid(), 3 | "name" character varying(255) not null, 4 | "slug" character varying(255) not null 5 | ); 6 | 7 | ALTER TABLE "public"."categories" ENABLE ROW LEVEL SECURITY; 8 | CREATE POLICY view_categories_policy ON "public"."categories" FOR SELECT 9 | USING (true); 10 | 11 | create table "public"."projects" ( 12 | "uuid" uuid not null default gen_random_uuid(), 13 | "projectId" character varying(255) default NULL, 14 | "title" character varying(255) not null, 15 | "ownerId" uuid references auth.users default auth.uid(), 16 | "excerpt" text not null, 17 | "description" text not null, 18 | "image" character varying(255) not null, 19 | "categoryUuid" uuid not null, 20 | "pledged" numeric not null default 0, 21 | "backers" integer not null default 0, 22 | "funded" character varying(255) not null default '0', 23 | "softCap" character varying(255) not null, 24 | "hardCap" character varying(255) not null, 25 | "startsAt" timestamp without time zone not null, 26 | "finishesAt" timestamp without time zone not null, 27 | "requestKey" character varying(255) not null default gen_random_uuid(), 28 | "status" character varying(255) not null default 'pending', 29 | "createdAt" timestamp without time zone not null default now(), 30 | "lastUpdatedAt" timestamp without time zone not null default now() 31 | ); 32 | 33 | ALTER TABLE "public"."projects" ENABLE ROW LEVEL SECURITY; 34 | CREATE POLICY view_projects_policy ON "public"."projects" FOR SELECT 35 | USING (true); 36 | 37 | CREATE POLICY "create_project_policy" 38 | ON "public"."projects" 39 | FOR INSERT 40 | TO authenticated 41 | WITH CHECK (true); 42 | 43 | CREATE POLICY "update_project_policy" 44 | ON "public"."projects" 45 | FOR UPDATE 46 | USING ( auth.uid() = uuid ); 47 | 48 | CREATE UNIQUE INDEX categories_pkey ON public.categories USING btree (uuid); 49 | 50 | CREATE UNIQUE INDEX projects_pkey ON public.projects USING btree (uuid); 51 | 52 | alter table "public"."categories" add constraint "categories_pkey" PRIMARY KEY using index "categories_pkey"; 53 | 54 | alter table "public"."projects" add constraint "projects_pkey" PRIMARY KEY using index "projects_pkey"; 55 | 56 | alter table "public"."projects" add constraint "projects_categoryUuid_fkey" FOREIGN KEY ("categoryUuid") REFERENCES categories(uuid) not valid; 57 | 58 | alter table "public"."projects" validate constraint "projects_categoryUuid_fkey"; 59 | 60 | CREATE POLICY "logged in users can upload project images 1iiiika_0" ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'projects'); -------------------------------------------------------------------------------- /pages/register.vue: -------------------------------------------------------------------------------- 1 | 34 | 89 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working 2 | # directory name when running `supabase init`. 3 | project_id = "forge-4-poc" 4 | 5 | [api] 6 | # Port to use for the API URL. 7 | port = 55321 8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 9 | # endpoints. public and storage are always included. 10 | schemas = ["public", "storage", "graphql_public"] 11 | # Extra schemas to add to the search_path of every request. public is always included. 12 | extra_search_path = ["public", "extensions"] 13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 14 | # for accidental or malicious requests. 15 | max_rows = 1000 16 | 17 | [db] 18 | # Port to use for the local database URL. 19 | port = 55322 20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 21 | # server_version;` on the remote database to check. 22 | major_version = 15 23 | 24 | [studio] 25 | # Port to use for Supabase Studio. 26 | port = 55323 27 | 28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 29 | # are monitored, and you can view the emails that would have been sent from the web interface. 30 | [inbucket] 31 | # Port to use for the email testing server web interface. 32 | port = 55324 33 | smtp_port = 55325 34 | pop3_port = 55326 35 | 36 | [storage] 37 | # The maximum file size allowed (e.g. "5MB", "500KB"). 38 | file_size_limit = "50MiB" 39 | 40 | [auth] 41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 42 | # in emails. 43 | site_url = "http://localhost:3000" 44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 45 | additional_redirect_urls = ["https://localhost:3000"] 46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one 47 | # week). 48 | jwt_expiry = 3600 49 | # Allow/disallow new user signups to your project. 50 | enable_signup = true 51 | 52 | [auth.email] 53 | # Allow/disallow new user signups via email to your project. 54 | enable_signup = true 55 | # If enabled, a user will be required to confirm any email change on both the old, and new email 56 | # addresses. If disabled, only the new email is required to confirm. 57 | double_confirm_changes = true 58 | # If enabled, users need to confirm their email address before signing in. 59 | enable_confirmations = false 60 | 61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`, 63 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 64 | [auth.external.apple] 65 | enabled = false 66 | client_id = "" 67 | secret = "" 68 | # Overrides the default auth redirectUrl. 69 | redirect_uri = "" 70 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 71 | # or any other third-party OIDC providers. 72 | url = "" 73 | 74 | [analytics] 75 | enabled = false 76 | port = 55327 77 | vector_port = 55328 78 | # Setup BigQuery project to enable log viewer on local development stack. 79 | # See: https://supabase.com/docs/guides/getting-started/local-development#enabling-local-logging 80 | gcp_project_id = "" 81 | gcp_project_number = "" 82 | gcp_jwt_path = "supabase/gcloud.json" 83 | -------------------------------------------------------------------------------- /components/ProjectsDetails.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 103 | -------------------------------------------------------------------------------- /composables/useWallet.ts: -------------------------------------------------------------------------------- 1 | import { getClient, IPactCommand, Pact } from "@kadena/client"; 2 | import { ICommand } from "@kadena/types"; 3 | 4 | const initialized = ref(false); 5 | const balance = ref(null); 6 | const networkId = ref("fast-development"); 7 | const chain = ref("0"); 8 | const instance = ref(null); 9 | const publicKey = ref(null); 10 | const account = ref(""); 11 | const isInstalled = ref(false); 12 | const isConnected = computed(() => account.value && publicKey.value); 13 | 14 | export function useWallet() { 15 | watch(isInstalled, (value) => { 16 | if (!value) { 17 | console.warn("Kadena Wallet is not installed"); 18 | } 19 | }); 20 | const checkIfWalletIsInstalled = () => { 21 | if (initialized.value) return; 22 | // @ts-expect-error - ecko wallet (kadena) may not be installed 23 | const { kadena } = window; 24 | isInstalled.value = Boolean(kadena && kadena.isKadena); 25 | instance.value = kadena; 26 | initialized.value = true; 27 | }; 28 | 29 | const connect = async () => { 30 | if (isInstalled.value && instance.value) { 31 | const { account, status } = await instance?.value.request({ 32 | method: "kda_connect", 33 | networkId: networkId.value, 34 | }); 35 | 36 | if (status === "success") { 37 | setAccount(account); 38 | } 39 | 40 | return { account, status }; 41 | } 42 | }; 43 | 44 | const setAccount = (data: { 45 | account: string | null; 46 | publicKey: string | null; 47 | }) => { 48 | account.value = data.account; 49 | publicKey.value = data.publicKey; 50 | }; 51 | 52 | const checkStatus = async () => { 53 | if (isInstalled.value && instance.value) { 54 | const { account, status } = await instance?.value.request({ 55 | method: "kda_checkStatus", 56 | networkId: networkId.value, 57 | }); 58 | if (status === "success") { 59 | setAccount(account); 60 | } 61 | 62 | return { account, status }; 63 | } 64 | }; 65 | 66 | const disconnect = async () => { 67 | if (isInstalled.value && instance.value) { 68 | if (isConnected.value) { 69 | const response = await instance?.value.request({ 70 | method: "kda_disconnect", 71 | networkId: networkId.value, 72 | }); 73 | console.log("disconnect response", response); 74 | setAccount({ account: null, publicKey: null }); 75 | return response; 76 | } 77 | } 78 | }; 79 | 80 | const getBalance = async () => { 81 | if (account.value) { 82 | // Exercise 1 starts here 83 | // Use the Kadena Client to get the balance 84 | const client = getClient( 85 | ({ chainId }) => 86 | `http://127.0.0.1:8080/chainweb/0.0/fast-development/chain/${chainId}/pact` 87 | ); 88 | const transaction = Pact.builder 89 | .execution(Pact.modules.coin["get-balance"](account.value)) 90 | .setMeta({ sender: account.value, chainId: chain.value }) 91 | .setNetworkId(networkId.value) 92 | .createTransaction(); 93 | 94 | const { result } = await client.dirtyRead(transaction); 95 | 96 | if (result.status === "failure") { 97 | throw new Error( 98 | `Can't get balance. Account (${account.value}) not found on network: ${networkId.value} and chain: ${chain.value}` 99 | ); 100 | } 101 | 102 | balance.value = result.data as string; 103 | } 104 | 105 | return balance.value; 106 | }; 107 | 108 | const signTransaction = async (transaction: any): Promise => { 109 | const response = await instance.value?.request({ 110 | method: "kda_requestQuickSign", 111 | data: { 112 | networkId: networkId.value, 113 | commandSigDatas: [ 114 | { 115 | sigs: [ 116 | { 117 | pubKey: publicKey.value, 118 | sig: undefined, 119 | }, 120 | ], 121 | cmd: transaction.cmd, 122 | }, 123 | ], 124 | }, 125 | }); 126 | 127 | if (transaction.hash !== response?.quickSignData?.[0]?.outcome?.hash) { 128 | throw Error("Hashes do not match!"); 129 | } 130 | 131 | return { 132 | cmd: transaction.cmd, 133 | sigs: response?.quickSignData?.[0]?.commandSigData?.sigs, 134 | hash: response?.quickSignData?.[0]?.outcome?.hash, 135 | }; 136 | }; 137 | 138 | watch(account, async (value) => { 139 | if (value) { 140 | await getBalance(); 141 | } 142 | }); 143 | 144 | checkIfWalletIsInstalled(); 145 | return { 146 | connect, 147 | disconnect, 148 | checkStatus, 149 | signTransaction, 150 | 151 | balance: computed(() => balance.value), 152 | isConnected, 153 | account: computed(() => account.value), 154 | publicKey: computed(() => publicKey.value), 155 | chain: computed(() => chain.value), 156 | networkId: computed(() => networkId.value), 157 | instance: computed(() => instance.value), 158 | isXWalletInstalled: computed(() => isInstalled.value), 159 | }; 160 | } 161 | -------------------------------------------------------------------------------- /supabase/schema.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json | undefined } 7 | | Json[] 8 | 9 | export interface Database { 10 | graphql_public: { 11 | Tables: { 12 | [_ in never]: never 13 | } 14 | Views: { 15 | [_ in never]: never 16 | } 17 | Functions: { 18 | graphql: { 19 | Args: { 20 | operationName?: string 21 | query?: string 22 | variables?: Json 23 | extensions?: Json 24 | } 25 | Returns: Json 26 | } 27 | } 28 | Enums: { 29 | [_ in never]: never 30 | } 31 | CompositeTypes: { 32 | [_ in never]: never 33 | } 34 | } 35 | public: { 36 | Tables: { 37 | categories: { 38 | Row: { 39 | name: string 40 | slug: string 41 | uuid: string 42 | } 43 | Insert: { 44 | name: string 45 | slug: string 46 | uuid?: string 47 | } 48 | Update: { 49 | name?: string 50 | slug?: string 51 | uuid?: string 52 | } 53 | Relationships: [] 54 | } 55 | projects: { 56 | Row: { 57 | backers: number 58 | categoryUuid: string 59 | createdAt: string 60 | description: string 61 | excerpt: string 62 | finishesAt: string 63 | funded: string 64 | hardCap: string 65 | image: string 66 | lastUpdatedAt: string 67 | ownerId: string | null 68 | pledged: number 69 | projectId: string | null 70 | requestKey: string 71 | softCap: string 72 | startsAt: string 73 | status: string 74 | title: string 75 | uuid: string 76 | } 77 | Insert: { 78 | backers?: number 79 | categoryUuid: string 80 | createdAt?: string 81 | description: string 82 | excerpt: string 83 | finishesAt: string 84 | funded?: string 85 | hardCap: string 86 | image: string 87 | lastUpdatedAt?: string 88 | ownerId?: string | null 89 | pledged?: number 90 | projectId?: string | null 91 | requestKey?: string 92 | softCap: string 93 | startsAt: string 94 | status?: string 95 | title: string 96 | uuid?: string 97 | } 98 | Update: { 99 | backers?: number 100 | categoryUuid?: string 101 | createdAt?: string 102 | description?: string 103 | excerpt?: string 104 | finishesAt?: string 105 | funded?: string 106 | hardCap?: string 107 | image?: string 108 | lastUpdatedAt?: string 109 | ownerId?: string | null 110 | pledged?: number 111 | projectId?: string | null 112 | requestKey?: string 113 | softCap?: string 114 | startsAt?: string 115 | status?: string 116 | title?: string 117 | uuid?: string 118 | } 119 | Relationships: [ 120 | { 121 | foreignKeyName: "projects_categoryUuid_fkey" 122 | columns: ["categoryUuid"] 123 | referencedRelation: "categories" 124 | referencedColumns: ["uuid"] 125 | }, 126 | { 127 | foreignKeyName: "projects_ownerId_fkey" 128 | columns: ["ownerId"] 129 | referencedRelation: "users" 130 | referencedColumns: ["id"] 131 | } 132 | ] 133 | } 134 | } 135 | Views: { 136 | [_ in never]: never 137 | } 138 | Functions: { 139 | [_ in never]: never 140 | } 141 | Enums: { 142 | [_ in never]: never 143 | } 144 | CompositeTypes: { 145 | [_ in never]: never 146 | } 147 | } 148 | storage: { 149 | Tables: { 150 | buckets: { 151 | Row: { 152 | allowed_mime_types: string[] | null 153 | avif_autodetection: boolean | null 154 | created_at: string | null 155 | file_size_limit: number | null 156 | id: string 157 | name: string 158 | owner: string | null 159 | public: boolean | null 160 | updated_at: string | null 161 | } 162 | Insert: { 163 | allowed_mime_types?: string[] | null 164 | avif_autodetection?: boolean | null 165 | created_at?: string | null 166 | file_size_limit?: number | null 167 | id: string 168 | name: string 169 | owner?: string | null 170 | public?: boolean | null 171 | updated_at?: string | null 172 | } 173 | Update: { 174 | allowed_mime_types?: string[] | null 175 | avif_autodetection?: boolean | null 176 | created_at?: string | null 177 | file_size_limit?: number | null 178 | id?: string 179 | name?: string 180 | owner?: string | null 181 | public?: boolean | null 182 | updated_at?: string | null 183 | } 184 | Relationships: [ 185 | { 186 | foreignKeyName: "buckets_owner_fkey" 187 | columns: ["owner"] 188 | referencedRelation: "users" 189 | referencedColumns: ["id"] 190 | } 191 | ] 192 | } 193 | migrations: { 194 | Row: { 195 | executed_at: string | null 196 | hash: string 197 | id: number 198 | name: string 199 | } 200 | Insert: { 201 | executed_at?: string | null 202 | hash: string 203 | id: number 204 | name: string 205 | } 206 | Update: { 207 | executed_at?: string | null 208 | hash?: string 209 | id?: number 210 | name?: string 211 | } 212 | Relationships: [] 213 | } 214 | objects: { 215 | Row: { 216 | bucket_id: string | null 217 | created_at: string | null 218 | id: string 219 | last_accessed_at: string | null 220 | metadata: Json | null 221 | name: string | null 222 | owner: string | null 223 | path_tokens: string[] | null 224 | updated_at: string | null 225 | version: string | null 226 | } 227 | Insert: { 228 | bucket_id?: string | null 229 | created_at?: string | null 230 | id?: string 231 | last_accessed_at?: string | null 232 | metadata?: Json | null 233 | name?: string | null 234 | owner?: string | null 235 | path_tokens?: string[] | null 236 | updated_at?: string | null 237 | version?: string | null 238 | } 239 | Update: { 240 | bucket_id?: string | null 241 | created_at?: string | null 242 | id?: string 243 | last_accessed_at?: string | null 244 | metadata?: Json | null 245 | name?: string | null 246 | owner?: string | null 247 | path_tokens?: string[] | null 248 | updated_at?: string | null 249 | version?: string | null 250 | } 251 | Relationships: [ 252 | { 253 | foreignKeyName: "objects_bucketId_fkey" 254 | columns: ["bucket_id"] 255 | referencedRelation: "buckets" 256 | referencedColumns: ["id"] 257 | } 258 | ] 259 | } 260 | } 261 | Views: { 262 | [_ in never]: never 263 | } 264 | Functions: { 265 | can_insert_object: { 266 | Args: { 267 | bucketid: string 268 | name: string 269 | owner: string 270 | metadata: Json 271 | } 272 | Returns: undefined 273 | } 274 | extension: { 275 | Args: { 276 | name: string 277 | } 278 | Returns: string 279 | } 280 | filename: { 281 | Args: { 282 | name: string 283 | } 284 | Returns: string 285 | } 286 | foldername: { 287 | Args: { 288 | name: string 289 | } 290 | Returns: unknown 291 | } 292 | get_size_by_bucket: { 293 | Args: Record 294 | Returns: { 295 | size: number 296 | bucket_id: string 297 | }[] 298 | } 299 | search: { 300 | Args: { 301 | prefix: string 302 | bucketname: string 303 | limits?: number 304 | levels?: number 305 | offsets?: number 306 | search?: string 307 | sortcolumn?: string 308 | sortorder?: string 309 | } 310 | Returns: { 311 | name: string 312 | id: string 313 | updated_at: string 314 | created_at: string 315 | last_accessed_at: string 316 | metadata: Json 317 | }[] 318 | } 319 | } 320 | Enums: { 321 | [_ in never]: never 322 | } 323 | CompositeTypes: { 324 | [_ in never]: never 325 | } 326 | } 327 | } 328 | 329 | -------------------------------------------------------------------------------- /composables/usePact.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getClient, 3 | ICommandResult, 4 | IPactCommand, 5 | isSignedCommand, 6 | literal, 7 | Pact, 8 | readKeyset, 9 | } from "@kadena/client"; 10 | import { ExtractType } from "@kadena/client/lib/commandBuilder/commandBuilder"; 11 | import { ICapabilityItem } from "@kadena/client/lib/interfaces/IPactCommand"; 12 | import { PactNumber } from "@kadena/pactjs"; 13 | import { RemovableRef, useStorage } from "@vueuse/core"; 14 | 15 | const kadenaClient = getClient( 16 | ({ chainId }) => 17 | `http://127.0.0.1:8080/chainweb/0.0/fast-development/chain/${chainId}/pact` 18 | ); 19 | const { chain, networkId, isConnected, account, publicKey } = useWallet(); 20 | export const usePact = async () => { 21 | const { signTransaction, connect, networkId, chain } = useWallet(); 22 | const pendingRequestsKeys: RemovableRef< 23 | Record 24 | > = useStorage("pendingRequestsKeys", {}); 25 | 26 | type Key = string; 27 | type Account = `${"k" | "w"}:${string}` | string; 28 | 29 | type Sender = { 30 | account: Account; 31 | publicKey: Key; 32 | }; 33 | 34 | type TFund = { 35 | projectId: string; 36 | funder: string; 37 | amount: number; 38 | }; 39 | 40 | type TProject = { 41 | id: string; 42 | name: string; 43 | sender: Sender; 44 | dates: { 45 | startsAt: Date; 46 | finishesAt: Date; 47 | }; 48 | hardCap: number; 49 | softCap: number; 50 | keyset: string; 51 | }; 52 | 53 | const createProjectObject = ({ 54 | id, 55 | name, 56 | sender, 57 | dates, 58 | hardCap, 59 | softCap, 60 | keyset, 61 | }: TProject) => { 62 | const cmd = Pact.modules["free.crowdfund"]["create-project"]( 63 | id, 64 | name, 65 | literal("coin"), 66 | new PactNumber(hardCap).toPactDecimal(), 67 | new PactNumber(softCap).toPactDecimal(), 68 | new Date(dates.startsAt), 69 | new Date(dates.finishesAt), 70 | sender.account, 71 | readKeyset(keyset) 72 | ); 73 | 74 | return cmd; 75 | }; 76 | 77 | type TTransactionOptions = { 78 | networkId?: string; 79 | chain?: IPactCommand["meta"]["chainId"]; 80 | }; 81 | 82 | const createFundObject = ({ projectId, funder, amount }: TFund) => { 83 | return Pact.modules["free.crowdfund"]["fund-project"]( 84 | projectId, 85 | funder, 86 | new PactNumber(amount).toPactDecimal() 87 | ); 88 | }; 89 | 90 | const _getOptions = ( 91 | options?: TTransactionOptions | TSignTransactionOptions 92 | ) => { 93 | return { 94 | networkId: options?.networkId || networkId.value, 95 | chain: options?.chain || chain.value, 96 | }; 97 | }; 98 | 99 | const createTransaction = ( 100 | executionObject: TCode, 101 | keyset: string, 102 | sender: Sender, 103 | options?: TTransactionOptions, 104 | capabilities?: ( 105 | withCapability: ExtractType<{ payload: { funs: [TCode] } }> 106 | ) => ICapabilityItem[] 107 | ) => { 108 | const { chain: chainId, networkId } = _getOptions(options); 109 | 110 | return Pact.builder 111 | .execution(executionObject) 112 | .addKeyset(keyset, "keys-all", sender.publicKey) 113 | .setNetworkId(networkId) //fast-development - https://github.com/kadena-community/crowdfund 114 | .setMeta({ chainId, sender: sender.account }) 115 | .addSigner(sender.publicKey, capabilities as any) 116 | .createTransaction(); 117 | }; 118 | 119 | const createSenderObject = (account: Account, publicKey: Key): Sender => { 120 | return { 121 | account: account, 122 | publicKey: publicKey, 123 | }; 124 | }; 125 | 126 | type TSignTransactionOptions = { 127 | networkId?: string; 128 | chain?: IPactCommand["meta"]["chainId"]; 129 | }; 130 | 131 | const submitCommand = async (signedTransaction: any) => { 132 | return await kadenaClient.submit(signedTransaction); 133 | }; 134 | 135 | const listen = async (requestKey: string): Promise => { 136 | return await kadenaClient.listen(requestKey, { 137 | networkId: networkId.value, 138 | chainId: chain.value, 139 | }); 140 | }; 141 | 142 | type TProjectForm = { 143 | id: string; 144 | name: string; 145 | startsAt: string; 146 | finishesAt: string; 147 | hardCap: string; 148 | softCap: string; 149 | }; 150 | 151 | type TFundForm = { 152 | id: string; 153 | amount: string; 154 | }; 155 | 156 | const create = async ( 157 | form: TProjectForm 158 | ): Promise<{ requestKey: string | null }> => { 159 | await connect(); 160 | try { 161 | if (!account.value || !publicKey.value) { 162 | throw new Error( 163 | "Account and public key are required to build transaction" 164 | ); 165 | } 166 | 167 | const sender = createSenderObject(account.value, publicKey.value); 168 | // this is from the wallet 169 | const keyset = "ks"; 170 | const cmd = createProjectObject({ 171 | id: form.id, 172 | name: form.name, 173 | sender, 174 | dates: { 175 | startsAt: new Date(form.startsAt), 176 | finishesAt: new Date(form.finishesAt), 177 | }, 178 | hardCap: Number(form.hardCap), 179 | softCap: Number(form.softCap), 180 | keyset, 181 | }); 182 | const transaction = createTransaction(cmd, keyset, sender); 183 | const signedCommand = await signTransaction(transaction); 184 | 185 | if (isSignedCommand(signedCommand)) { 186 | const requestKey = await submitCommand(signedCommand); 187 | saveToRequestKeyLocalStorage(requestKey, "create"); 188 | pollPendingRequests().then(() => null); 189 | return { requestKey }; 190 | } 191 | } catch (err) { 192 | console.log(err); 193 | // alert there was an error submitting to blockchain 194 | } 195 | 196 | return { requestKey: null }; 197 | }; 198 | 199 | const fund = async ( 200 | form: TFundForm 201 | ): Promise<{ requestKey: string | null }> => { 202 | await connect(); 203 | try { 204 | if (!account.value || !publicKey.value) { 205 | throw new Error( 206 | "Account and public key are required to build transaction" 207 | ); 208 | } 209 | 210 | const sender = createSenderObject(account.value, publicKey.value); 211 | // this is from the wallet 212 | const keyset = "ks"; 213 | const cmd = createFundObject({ 214 | projectId: form.id, 215 | funder: sender.account, 216 | amount: Number(form.amount), 217 | }); 218 | const transaction = createTransaction( 219 | cmd, 220 | keyset, 221 | sender, 222 | {}, 223 | (withCapability) => [ 224 | withCapability("coin.GAS"), 225 | withCapability("coin.TRANSFER"), 226 | ] 227 | ); 228 | const signedCommand = await signTransaction(transaction); 229 | 230 | if (isSignedCommand(signedCommand)) { 231 | const requestKey = await submitCommand(signedCommand); 232 | saveToRequestKeyLocalStorage(requestKey, "fund"); 233 | pollPendingRequests().then(() => null); 234 | return { requestKey }; 235 | } 236 | } catch (err) { 237 | console.log(err); 238 | // alert there was an error submitting to blockchain 239 | } 240 | 241 | return { requestKey: null }; 242 | }; 243 | 244 | const saveToRequestKeyLocalStorage = ( 245 | requestKey: string, 246 | type: "create" | "fund" 247 | ) => { 248 | const currentRequestKeys: Record< 249 | string, 250 | { requestKey: string; type: string } 251 | > = pendingRequestsKeys.value; 252 | currentRequestKeys[requestKey] = { requestKey, type }; 253 | pendingRequestsKeys.value = currentRequestKeys; 254 | }; 255 | 256 | const updatePendingRequestStatus = ( 257 | pendingRequest: ICommandResult, 258 | pending: { requestKey: string; type: string } 259 | ) => { 260 | const { updateStatusForRequestKey } = useProjects(); 261 | // // update project status on DB with response from blockchain 262 | if (pendingRequest.result.status === "success") { 263 | let data = {}; 264 | // If the pending request is a create project 265 | if (pending.type === "create") { 266 | data = { status: "created" }; 267 | } 268 | // If the pending request is a fund project 269 | if (pending.type === "fund") { 270 | // Add +1 Backer 271 | data = { backers: 1 }; 272 | } 273 | const response = updateStatusForRequestKey(pendingRequest.reqKey, data); 274 | console.log("success", response); 275 | // data has been stored in the blockchain 276 | // we can connect it with the data in the database 277 | // remove from pending requests 278 | } else { 279 | // there was an error alert 280 | } 281 | }; 282 | 283 | const pollPendingRequests = async () => { 284 | if (kadenaClient) { 285 | for (const pending of Object.values(pendingRequestsKeys.value)) { 286 | const response = await listen(pending.requestKey); 287 | updatePendingRequestStatus(response, pending); 288 | } 289 | } 290 | }; 291 | 292 | const getProjectStatus = async (uuid: string) => { 293 | const transaction = Pact.builder 294 | .execution(Pact.modules["free.crowdfund"]["read-project"](uuid)) 295 | .setNetworkId(networkId.value) //fast-development - https://github.com/kadena-community/crowdfund 296 | .setMeta({ 297 | chainId: chain.value, // instruct everyone to use chain 0 on devnet 298 | }) 299 | .createTransaction(); 300 | const response = await kadenaClient.dirtyRead(transaction); 301 | console.log("response", response); 302 | 303 | return response; 304 | }; 305 | 306 | return { 307 | create, 308 | fund, 309 | pollPendingRequests, 310 | getProjectStatus, 311 | listen, 312 | }; 313 | }; 314 | -------------------------------------------------------------------------------- /components/ProjectsForm.vue: -------------------------------------------------------------------------------- 1 | 121 | 122 | 290 | -------------------------------------------------------------------------------- /crowdfund.pact: -------------------------------------------------------------------------------- 1 | (namespace (read-msg "ns")) 2 | 3 | (module crowdfund GOVERNANCE 4 | 5 | ; -------------------------------------------------------------------------- 6 | ; Schemas and Tables 7 | 8 | ;define projects schema 9 | (defschema projects 10 | projectId:string 11 | title:string 12 | token:module{fungible-v2} 13 | hardCap:decimal 14 | softCap:decimal 15 | raised:decimal 16 | startDate:time 17 | endDate:time 18 | status:integer 19 | project-owner:string 20 | project-owner-guard:guard 21 | ) 22 | 23 | ;define funds schema 24 | (defschema funds 25 | projectId:string 26 | fundOwner:string 27 | status:integer 28 | amount:decimal 29 | timestamp:time 30 | ) 31 | 32 | (deftable projects-table:{projects}) 33 | (deftable funds-table:{funds}) 34 | 35 | 36 | ; -------------------------------------------------------------------------- 37 | ; Constants 38 | 39 | ; Statusses 40 | (defconst CREATED 0) 41 | (defconst CANCELLED 1) 42 | (defconst SUCCEEDED 2) 43 | (defconst FAILED 3) 44 | 45 | 46 | ; -------------------------------------------------------------------------- 47 | ; Utils 48 | 49 | (defun vault-guard:guard (projectId: string) (create-capability-guard (VAULT_GUARD projectId))) 50 | 51 | (defun vault-account:string (projectId:string) 52 | (create-principal (create-capability-guard (VAULT_GUARD projectId))) 53 | ) 54 | 55 | (defun get-fund-key:string (projectId:string account:string) (format "{}-{}" [projectId account])) 56 | 57 | (defun curr-time:time () 58 | @doc "Returns current chain's block-time in time type" 59 | (at 'block-time (chain-data))) 60 | 61 | 62 | ; ; -------------------------------------------------------------------------- 63 | ; ; Capabilities 64 | 65 | (defcap GOVERNANCE () 66 | @doc " Give the admin full access to call and upgrade the module." 67 | (enforce-keyset "free.crowdfund-admin") 68 | ) 69 | 70 | (defcap ACCT_GUARD (account:string projectId:string) 71 | (with-read projects-table projectId { 72 | "token":=token:module{fungible-v2} 73 | } 74 | (enforce-guard (at 'guard (token::details account))) 75 | ) 76 | ) 77 | 78 | (defcap PROJECT_OPEN:bool (projectId:string) 79 | (with-read projects-table projectId { 80 | "startDate":=startDate, 81 | "endDate":=endDate, 82 | "status":=status 83 | } 84 | 85 | (enforce (= status CREATED) "PROJECT HAS EITHER BEEN CANCELLED OR COMPLETED") 86 | (enforce (< (curr-time) endDate) "PROJECT HAS ENDED") 87 | (enforce (>= (curr-time) startDate) "PROJECT HAS NOT YET STARTED") 88 | ) 89 | ) 90 | 91 | (defcap REFUND (projectId from:string) 92 | @doc "Capability that validates if an investment can be refunded" 93 | (with-read projects-table projectId { 94 | "startDate":=startDate, 95 | "status":= status, 96 | "token":= token:module{fungible-v2} 97 | } 98 | (let ((funder-guard (at 'guard (token::details from)) 99 | )) 100 | (enforce-guard funder-guard) 101 | (enforce (>= (curr-time) startDate) "PROJECT HAS NOT STARTED") 102 | (enforce (!= status SUCCEEDED) "PROJECT HAS ALREADY SUCCEEDED") 103 | ) 104 | ) 105 | ) 106 | 107 | (defcap CANCEL:bool (projectId:string) 108 | @doc "Capability that validates if a project can be cancelled" 109 | (with-read projects-table projectId { 110 | "status":=status, 111 | "startDate":=startDate 112 | } 113 | (enforce (!= status CANCELLED) "NOT CANCELLED") 114 | (enforce (< (curr-time) startDate) "PROJECT HAS ALREADY STARTED, CANNOT CANCEL") 115 | ) 116 | ) 117 | 118 | (defcap SUCCESS:bool (projectId) 119 | @doc "Capability that validates if a project can be marked as a success" 120 | (with-read projects-table projectId{ 121 | "softCap":=softCap, 122 | "hardCap":=hardCap, 123 | "raised":=raised, 124 | "endDate":=endDate, 125 | "status":=status 126 | } 127 | 128 | (enforce (or (>= (curr-time) endDate) (>= raised hardCap)) "PROJECT HAS NOT ENDED OR HARDCAP NOT MET") 129 | (enforce (>= raised softCap) "PROJECT HAS NOT RAISED ENOUGH") 130 | (enforce (!= status CANCELLED) "PROJECT HAS BEEN CANCELLED") 131 | ) 132 | ) 133 | 134 | (defcap FAIL:bool (projectId) 135 | @doc "Capability that validates if a project can be marked as a failure" 136 | (with-read projects-table projectId{ 137 | "softCap":=softCap, 138 | "endDate":=endDate, 139 | "raised":=raised, 140 | "status":=status 141 | } 142 | (enforce (!= status CANCELLED) "PROJECT HAS BEEN CANCELLED") 143 | (enforce (>= (curr-time) endDate) "PROJECT HAS NOT ENDED") 144 | (enforce (< raised softCap) "PROJECT HAS SUCCEEDED")) 145 | ) 146 | 147 | (defcap PROJECT_OWNER:bool (projectId) 148 | @doc "Capability that validates if the user is the owner of the project" 149 | (with-read projects-table projectId { 150 | "project-owner-guard":=project-owner-guard 151 | } 152 | (enforce-guard project-owner-guard)) 153 | ) 154 | 155 | (defcap VAULT_GUARD:bool (project-id:string) true) 156 | 157 | (defcap DECREASE_RAISE () true) 158 | (defcap INCREASE_RAISE () true) 159 | 160 | ; -------------------------------------------------------------------------- 161 | ; Project configuration functions 162 | 163 | (defun create-project ( 164 | projectId:string 165 | title:string 166 | token:module{fungible-v2} 167 | hardCap:decimal 168 | softCap:decimal 169 | startDate:time 170 | endDate:time 171 | project-owner:string 172 | project-owner-guard:guard) 173 | "Adds a project to projects table" 174 | (enforce (< (curr-time) startDate) "Start Date shouldn't be in the past") 175 | (enforce (< startDate endDate) "Start Date should be before end date") 176 | (enforce (< 0.0 hardCap) "Hard cap is not a positive number") 177 | (enforce (< 0.0 softCap) "Soft cap is not a positive number") 178 | (enforce (< softCap hardCap) "Hardcap should be higher than softcap") 179 | 180 | (insert projects-table projectId { 181 | "projectId":projectId, 182 | "title":title, 183 | "hardCap":hardCap, 184 | "softCap":softCap, 185 | "token":token, 186 | "raised": 0.0, 187 | "startDate":startDate, 188 | "endDate": endDate, 189 | "status": CREATED, 190 | "project-owner": project-owner, 191 | "project-owner-guard": project-owner-guard 192 | }) 193 | ) 194 | 195 | (defun cancel-project (projectId) 196 | (with-capability (PROJECT_OWNER projectId) 197 | (with-capability (CANCEL projectId) 198 | (update projects-table projectId { 199 | "status": CANCELLED 200 | }))) 201 | ) 202 | 203 | (defun succeed-project (projectId) 204 | (with-capability (PROJECT_OWNER projectId) 205 | (with-capability (SUCCESS projectId) 206 | (update projects-table projectId { 207 | "status": SUCCEEDED 208 | }) 209 | (with-read projects-table projectId { 210 | "raised":= raised, 211 | "token":= token:module{fungible-v2}, 212 | "project-owner":= project-owner, 213 | "project-owner-guard":= project-owner-guard 214 | } 215 | 216 | (with-capability (VAULT_GUARD projectId) 217 | (install-capability (token::TRANSFER (vault-account projectId) project-owner raised)) 218 | (token::transfer-create (vault-account projectId) project-owner project-owner-guard raised) 219 | ) 220 | ) 221 | ) 222 | ) 223 | ) 224 | 225 | (defun fail-project (projectId) 226 | (with-capability (PROJECT_OWNER projectId) 227 | (with-capability (FAIL projectId) 228 | (update projects-table projectId { 229 | "status": FAILED 230 | }))) 231 | ) 232 | 233 | ; -------------------------------------------------------------------------- 234 | ; Fundraising functions 235 | 236 | (defun create-fund (projectId funder amount) 237 | (require-capability (INCREASE_RAISE)) 238 | (with-default-read funds-table (get-fund-key projectId funder) 239 | { 240 | "amount": 0.0 241 | } 242 | { 243 | "amount":= fundedAmount 244 | } 245 | (write funds-table (get-fund-key projectId funder) { 246 | "projectId":projectId, 247 | "fundOwner":funder, 248 | "amount":(+ amount fundedAmount), 249 | "timestamp":(curr-time), 250 | "status":CREATED 251 | })) 252 | ) 253 | 254 | (defun cancel-fund (projectId funder) 255 | (require-capability (DECREASE_RAISE)) 256 | (update funds-table (get-fund-key projectId funder) { 257 | "status":CANCELLED, 258 | "amount": 0.0 259 | })) 260 | 261 | (defun increase-project-raise (projectId amount) 262 | (require-capability (INCREASE_RAISE)) 263 | (with-read projects-table projectId { 264 | "raised":= raised 265 | } 266 | (update projects-table projectId { 267 | "raised": (+ raised amount) 268 | })) 269 | ) 270 | 271 | (defun decrease-project-raise (projectId amount) 272 | (require-capability (DECREASE_RAISE)) 273 | (with-read projects-table projectId { 274 | "raised":= raised 275 | } 276 | (update projects-table projectId { 277 | "raised": (- raised amount) 278 | } 279 | ) 280 | ) 281 | ) 282 | 283 | (defun fund-project (projectId funder amount) 284 | (with-capability (INCREASE_RAISE) 285 | (with-read projects-table projectId { 286 | "raised":= raised, 287 | "hardCap":= hardCap, 288 | "token":= token:module{fungible-v2} 289 | } 290 | (with-capability (PROJECT_OPEN projectId) 291 | (with-capability (ACCT_GUARD funder projectId) 292 | (let* 293 | ( 294 | (remainingProjectCap (- hardCap raised)) 295 | (fundAmount (if (< amount remainingProjectCap) amount remainingProjectCap)) 296 | ) 297 | 298 | (enforce (< raised hardCap) "HARDCAP REACHED") 299 | (enforce (> fundAmount 0.0) "Zero investment amount") 300 | 301 | (token::transfer-create funder (vault-account projectId) (vault-guard projectId) fundAmount) 302 | (create-fund projectId funder fundAmount) 303 | (increase-project-raise projectId fundAmount) 304 | ) 305 | ))))) 306 | 307 | (defun rollback-fund-project (projectId funder) 308 | (with-capability (DECREASE_RAISE) 309 | (with-capability (REFUND projectId funder) 310 | (with-read projects-table projectId { 311 | "token":= token:module{fungible-v2} 312 | } 313 | (with-capability (ACCT_GUARD funder projectId) 314 | (with-read funds-table (get-fund-key projectId funder) { 315 | "amount":= amount, 316 | "status":= status 317 | } 318 | (enforce (= status CREATED) "NO ACTIVE FUNDS") 319 | 320 | (cancel-fund projectId funder) 321 | (decrease-project-raise projectId amount) 322 | 323 | (with-capability (VAULT_GUARD projectId) 324 | (install-capability (token::TRANSFER (vault-account projectId) funder amount)) 325 | (token::transfer (vault-account projectId) funder amount) 326 | ) 327 | ) 328 | ) 329 | )))) 330 | 331 | ; -------------------------------------------------------------------------- 332 | ; State functions 333 | 334 | (defun fetch-user-fundings:list (account:string) 335 | (map (fetch-funded-amount account) (keys projects-table)) 336 | ) 337 | 338 | (defun fetch-funded-amount (projectId:string fundOwner:string) 339 | (with-default-read funds-table (get-fund-key projectId fundOwner) 340 | { 341 | "amount": 0.0 342 | } 343 | { 344 | "amount":= fundedAmount 345 | } 346 | { "fundedAmount": fundedAmount, "projectId": projectId } 347 | ) 348 | ) 349 | 350 | (defun read-project (projectId) 351 | (read projects-table projectId) 352 | ) 353 | 354 | (defun read-projects:list () 355 | "Read all projects in projects table" 356 | (select projects-table 357 | ['projectId 'title 'hardCap 'softCap 'token 'raised 'startDate 'endDate 'status] 358 | (constantly true) 359 | ) 360 | ) 361 | ) 362 | 363 | 364 | ; -------------------------------------------------------------------------- 365 | ; Deployment 366 | 367 | (if (read-msg "upgrade") 368 | [] 369 | [ 370 | (define-keyset "free.crowdfund-admin" (read-keyset "admin-keyset")) 371 | (create-table projects-table) 372 | (create-table funds-table) 373 | ] 374 | ) 375 | -------------------------------------------------------------------------------- /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO 2 | categories (uuid, name, slug) 3 | VALUES 4 | ( 5 | '45f42df8-3e1d-4b2d-8f80-7c35806d5739', 6 | 'Technology', 7 | 'technology' 8 | ), 9 | ( 10 | 'ac52d4f3-20bf-4956-8305-6b9870bb8593', 11 | 'Art', 12 | 'art' 13 | ), 14 | ( 15 | 'ea22c5ed-3cf6-4ff1-9e32-9f2f39f32c94', 16 | 'Music', 17 | 'music' 18 | ), 19 | ( 20 | '0d09f204-921e-4e96-bc78-4e3b9357f6a7', 21 | 'Fashion', 22 | 'fashion' 23 | ), 24 | ( 25 | 'e95a8fdd-3e14-48b6-a4f6-6f6625a4576c', 26 | 'Food', 27 | 'food' 28 | ), 29 | ( 30 | 'c5c0d889-0676-4411-92e3-b8c30c43260e', 31 | 'Film & Video', 32 | 'film_video' 33 | ), 34 | ( 35 | '788f13ac-45aa-44b7-98f1-48c669b1a27a', 36 | 'Books', 37 | 'books' 38 | ), 39 | ( 40 | '3a8ef95f-3c99-4192-8197-7cb289d23933', 41 | 'Design', 42 | 'design' 43 | );-- Insert seed data for 'projects' table 44 | INSERT INTO 45 | projects ( 46 | uuid, 47 | title, 48 | excerpt, 49 | description, 50 | image, 51 | "categoryUuid", 52 | pledged, 53 | backers, 54 | funded, 55 | "softCap", 56 | "hardCap", 57 | "startsAt", 58 | "finishesAt", 59 | "createdAt", 60 | "lastUpdatedAt" 61 | ) 62 | VALUES 63 | ( 64 | '3f1808ba-7fea-4b9b-9c11-3d2aeacedade', 65 | 'Smartwatch Project', 66 | 'Revolutionary smartwatch', 67 | 'This smartwatch project aims to bring the latest technology to your wrist.', 68 | 'https://loremflickr.com/1000/640/smartwatch', 69 | '45f42df8-3e1d-4b2d-8f80-7c35806d5739', 70 | 5000.00, 71 | 120, 72 | '15000.00', 73 | '20000.00', 74 | '50000.00', 75 | '2023-07-22 23:59:59', 76 | '2023-08-31 23:59:59', 77 | '2023-07-15 10:30:00', 78 | '2023-07-15 10:30:00' 79 | ), 80 | ( 81 | '13ad8677-a09b-4007-af50-469dde6077e3', 82 | 'Art Exhibition', 83 | 'Contemporary art exhibition', 84 | 'An art exhibition showcasing the works of talented artists.', 85 | 'https://loremflickr.com/1000/640/art_exhibition', 86 | 'ac52d4f3-20bf-4956-8305-6b9870bb8593', 87 | 300.00, 88 | 50, 89 | '4000.00', 90 | '4000.00', 91 | '8000.00', 92 | '2023-07-12 23:59:59', 93 | '2023-09-15 23:59:59', 94 | '2023-07-16 09:45:00', 95 | '2023-07-16 09:45:00' 96 | ), 97 | ( 98 | '97236007-adcb-483e-aac8-e099d9f82318', 99 | 'Album Recording', 100 | 'Debut album recording', 101 | 'Support the recording of our debut music album.', 102 | 'https://loremflickr.com/1000/640/album_recording', 103 | 'ea22c5ed-3cf6-4ff1-9e32-9f2f39f32c94', 104 | 8000.00, 105 | 90, 106 | '7000.00', 107 | '6000.00', 108 | '10000.00', 109 | '2023-07-02 23:59:59', 110 | '2023-10-10 23:59:59', 111 | '2023-07-16 14:20:00', 112 | '2023-07-16 14:20:00' 113 | ), 114 | ( 115 | '256a3745-6219-4c88-9549-9db9145bd141', 116 | 'Fashion Clothing Line', 117 | 'New fashion clothing line', 118 | 'Launching a trendy clothing line with unique designs and styles.', 119 | 'https://loremflickr.com/1000/640/fashion_clothing', 120 | '0d09f204-921e-4e96-bc78-4e3b9357f6a7', 121 | 1040.00, 122 | 80, 123 | '05000.00', 124 | '12000.00', 125 | '25000.00', 126 | '2023-07-22 23:59:59', 127 | '2023-11-30 23:59:59', 128 | '2023-07-17 10:30:00', 129 | '2023-07-17 10:30:00' 130 | ), 131 | ( 132 | '4db2eead-bebb-4c13-862d-2d658fe42291', 133 | 'Organic Food Delivery', 134 | 'Fresh organic food delivery service', 135 | 'Starting a sustainable and healthy food delivery business.', 136 | 'https://loremflickr.com/1000/640/food_delivery', 137 | 'e95a8fdd-3e14-48b6-a4f6-6f6625a4576c', 138 | 1000.00, 139 | 60, 140 | '7000.00', 141 | '6000.00', 142 | '10000.00', 143 | '2023-07-20 23:59:59', 144 | '2023-10-20 23:59:59', 145 | '2023-07-17 11:45:00', 146 | '2023-07-17 11:45:00' 147 | ), 148 | ( 149 | '1204de01-7bb8-46a2-91fc-386d5665befc', 150 | 'Indie Short Film', 151 | 'Short film production', 152 | 'Creating an indie short film exploring a thought-provoking story.', 153 | 'https://loremflickr.com/1000/640/movie', 154 | 'c5c0d889-0676-4411-92e3-b8c30c43260e', 155 | 50.00, 156 | 40, 157 | '4000.00', 158 | '4000.00', 159 | '8000.00', 160 | '2023-07-31 23:59:59', 161 | '2023-09-25 23:59:59', 162 | '2023-07-17 14:20:00', 163 | '2023-07-17 14:20:00' 164 | ), 165 | ( 166 | 'e4b9ca3a-ed1e-45df-928d-7c514d81cd4c', 167 | 'Sweet New Board Game', 168 | 'A board game that is trendy and fun.', 169 | 'Creating a new board game that is fun and engaging for all ages.', 170 | 'https://loremflickr.com/1000/640/board_game', 171 | '45f42df8-3e1d-4b2d-8f80-7c35806d5739', 172 | 800.00, 173 | 70, 174 | '7000.00', 175 | '6000.00', 176 | '10000.00', 177 | '2023-07-06 23:59:59', 178 | '2023-09-18 23:59:59', 179 | '2023-07-20 09:45:00', 180 | '2023-07-20 09:45:00' 181 | ), 182 | ( 183 | '1d8e7942-3eb8-4506-9cea-09d6230325e1', 184 | 'External Laptop Monitor', 185 | 'A portable monitor for your laptop.', 186 | 'Creating a portable monitor that can be used with any laptop.', 187 | 'https://loremflickr.com/1000/640/laptop', 188 | 'ac52d4f3-20bf-4956-8305-6b9870bb8593', 189 | 4000.00, 190 | 40, 191 | '3000.00', 192 | '3000.00', 193 | '6000.00', 194 | '2023-07-05 23:59:59', 195 | '2023-10-05 23:59:59', 196 | '2023-07-21 11:00:00', 197 | '2023-07-21 11:00:00' 198 | ), 199 | ( 200 | 'd717d859-4065-4d38-8018-747b23f50c05', 201 | 'Tech Gadget Innovation', 202 | 'Innovative tech gadgets', 203 | 'Developing cutting-edge tech gadgets for everyday use.', 204 | 'https://loremflickr.com/1000/640/tech_gadgets', 205 | '45f42df8-3e1d-4b2d-8f80-7c35806d5739', 206 | 8000.00, 207 | 70, 208 | '7000.00', 209 | '6000.00', 210 | '10000.00', 211 | '2023-07-22 23:59:59', 212 | '2023-09-18 23:59:59', 213 | '2023-07-20 09:45:00', 214 | '2023-07-20 09:45:00' 215 | ), 216 | ( 217 | '7b9c2a7a-7058-4a7b-9f3b-8fde35de45ac', 218 | 'Art Therapy Workshops', 219 | 'Art therapy for mental health', 220 | 'Organizing art therapy workshops to promote mental well-being.', 221 | 'https://loremflickr.com/1000/640/art_therapy', 222 | 'ac52d4f3-20bf-4956-8305-6b9870bb8593', 223 | 4000.00, 224 | 40, 225 | '3000.00', 226 | '3000.00', 227 | '6000.00', 228 | '2023-07-02 23:59:59', 229 | '2023-10-05 23:59:59', 230 | '2023-07-21 11:00:00', 231 | '2023-07-21 11:00:00' 232 | ), 233 | ( 234 | 'f672a67c-87e0-43b9-bbe2-a660e45cb8f1', 235 | 'Rock Music Album', 236 | 'Bangin rock music album', 237 | 'Recording an album featuring sweet guitar rifts and banging drums.', 238 | 'https://loremflickr.com/1000/640/rock_music', 239 | 'ea22c5ed-3cf6-4ff1-9e32-9f2f39f32c94', 240 | 6000.00, 241 | 80, 242 | '5000.00', 243 | '5000.00', 244 | '8000.00', 245 | '2023-07-22 23:59:59', 246 | '2023-11-05 23:59:59', 247 | '2023-07-22 14:30:00', 248 | '2023-07-22 14:30:00' 249 | ), 250 | ( 251 | '8c0be7f1-981c-40f0-a05c-22917c5bd5de', 252 | 'Eco-Friendly Books', 253 | 'Eco-conscious book publishing', 254 | 'Publishing a series of eco-friendly books promoting sustainable living.', 255 | 'https://loremflickr.com/1000/640/books', 256 | '788f13ac-45aa-44b7-98f1-48c669b1a27a', 257 | 3500.00, 258 | 50, 259 | '2500.00', 260 | '3000.00', 261 | '5000.00', 262 | '2023-07-22 23:59:59', 263 | '2023-09-25 23:59:59', 264 | '2023-07-23 10:15:00', 265 | '2023-07-23 10:15:00' 266 | ), 267 | ( 268 | '0470fdae-9ae0-4902-88e7-6a8502505e4e', 269 | 'Urban Farming Initiative', 270 | 'Community urban farming project', 271 | 'Creating a community-driven urban farming project to grow fresh produce.', 272 | 'https://loremflickr.com/1000/640/urban_farming', 273 | 'e95a8fdd-3e14-48b6-a4f6-6f6625a4576c', 274 | 5000.00, 275 | 40, 276 | '4000.00', 277 | '4000.00', 278 | '7000.00', 279 | '2023-07-18 23:59:59', 280 | '2023-10-15 23:59:59', 281 | '2023-07-24 11:30:00', 282 | '2023-07-24 11:30:00' 283 | ), 284 | ( 285 | '2f753fc0-131c-48c2-b97e-62fe65957709', 286 | 'Indie Video Game', 287 | 'Adventure game development', 288 | 'Creating an indie video game with captivating storyline and gameplay.', 289 | 'https://loremflickr.com/1000/640/video_game_controller', 290 | 'c5c0d889-0676-4411-92e3-b8c30c43260e', 291 | 12000.00, 292 | 90, 293 | '02000.00', 294 | '10000.00', 295 | '15000.00', 296 | '2023-08-18 23:59:59', 297 | '2023-11-30 23:59:59', 298 | '2023-07-25 12:45:00', 299 | '2023-07-25 12:45:00' 300 | ), 301 | ( 302 | '461ec726-067e-4d4b-9ab2-14b17eecda45', 303 | 'Cookbook Publication', 304 | 'Gourmet cookbook publishing', 305 | 'Publishing a gourmet cookbook with a collection of unique recipes.', 306 | 'https://loremflickr.com/1000/640/cookbook', 307 | '3a8ef95f-3c99-4192-8197-7cb289d23933', 308 | 6000.00, 309 | 60, 310 | '5000.00', 311 | '5000.00', 312 | '8000.00', 313 | '2023-07-18 23:59:59', 314 | '2023-11-10 23:59:59', 315 | '2023-07-26 14:20:00', 316 | '2023-07-26 14:20:00' 317 | ), 318 | ( 319 | '00e5fe63-b26c-40e2-a3ac-eb3780932a46', 320 | 'Eco-Friendly Fashion', 321 | 'Sustainable fashion collection', 322 | 'Launching an eco-conscious fashion collection with recycled materials.', 323 | 'https://loremflickr.com/1000/640/eco_friendly_fashion', 324 | '0d09f204-921e-4e96-bc78-4e3b9357f6a7', 325 | 10000.00, 326 | 100, 327 | '00000.00', 328 | '8000.00', 329 | '12000.00', 330 | '2023-08-18 23:59:59', 331 | '2023-10-20 23:59:59', 332 | '2023-07-27 15:30:00', 333 | '2023-07-27 15:30:00' 334 | ), 335 | ( 336 | 'b831f62b-4f0b-4ff9-9c11-266da08627dd', 337 | 'Community Art Project', 338 | 'Public art installations', 339 | 'Creating interactive art installations to engage the community.', 340 | 'https://loremflickr.com/1000/640/community_art_project', 341 | 'ac52d4f3-20bf-4956-8305-6b9870bb8593', 342 | 4000.00, 343 | 40, 344 | '3000.00', 345 | '3000.00', 346 | '6000.00', 347 | '2023-08-18 23:59:59', 348 | '2023-09-05 23:59:59', 349 | '2023-07-28 17:00:00', 350 | '2023-07-28 17:00:00' 351 | ), 352 | ( 353 | 'c3ad414e-1864-4097-a7af-0a35e29b5ad3', 354 | 'Acoustic Music Album', 355 | 'Soothing acoustic melodies', 356 | 'Recording an acoustic music album featuring soothing melodies.', 357 | 'https://loremflickr.com/1000/640/acoustic_music', 358 | 'ea22c5ed-3cf6-4ff1-9e32-9f2f39f32c94', 359 | 5000.00, 360 | 70, 361 | '4000.00', 362 | '4000.00', 363 | '8000.00', 364 | '2023-08-18 23:59:59', 365 | '2023-09-15 23:59:59', 366 | '2023-07-29 10:30:00', 367 | '2023-07-29 10:30:00' 368 | ), 369 | ( 370 | '78bbe66e-b9e7-48a4-97da-a74bf5161611', 371 | 'Healthy Meal Kits', 372 | 'Nutritious meal kits delivery', 373 | 'Providing convenient meal kits with healthy and delicious recipes.', 374 | 'https://loremflickr.com/1000/640/food', 375 | 'e95a8fdd-3e14-48b6-a4f6-6f6625a4576c', 376 | 3000.00, 377 | 40, 378 | '2000.00', 379 | '2500.00', 380 | '5000.00', 381 | '2023-08-18 23:59:59', 382 | '2023-09-10 23:59:59', 383 | '2023-07-30 12:15:00', 384 | '2023-07-30 12:15:00' 385 | ), 386 | ( 387 | 'e69cf89a-b6ad-4f9d-8fca-544eac66feb8', 388 | 'DIY Home Improvement', 389 | 'Home improvement tutorials', 390 | 'Creating DIY home improvement tutorials for enthusiasts.', 391 | 'https://loremflickr.com/1000/640/hammer', 392 | '3a8ef95f-3c99-4192-8197-7cb289d23933', 393 | 8000.00, 394 | 70, 395 | '7000.00', 396 | '6000.00', 397 | '10000.00', 398 | '2023-08-18 23:59:59', 399 | '2023-11-08 23:59:59', 400 | '2023-07-31 14:45:00', 401 | '2023-07-31 14:45:00' 402 | ), 403 | ( 404 | '78dcf11a-c80f-4f04-9d4b-e487426deb6e', 405 | 'Charity Run Event', 406 | 'Fundraising charity run', 407 | 'Organizing a charity run event to raise funds for a noble cause.', 408 | 'https://loremflickr.com/1000/640/run', 409 | '45f42df8-3e1d-4b2d-8f80-7c35806d5739', 410 | 4000.00, 411 | 50, 412 | '3000.00', 413 | '3000.00', 414 | '6000.00', 415 | '2023-08-18 23:59:59', 416 | '2023-10-25 23:59:59', 417 | '2023-08-01 16:30:00', 418 | '2023-08-01 16:30:00' 419 | ), 420 | ( 421 | 'abd1482b-9e32-44a8-b5ef-06f40e121ca8', 422 | 'Tech Coding Bootcamp', 423 | 'Coding education program', 424 | 'Launching a coding bootcamp to educate aspiring developers.', 425 | 'https://loremflickr.com/1000/640/coding_bootcamp', 426 | '45f42df8-3e1d-4b2d-8f80-7c35806d5739', 427 | 10000.00, 428 | 90, 429 | '00000.00', 430 | '8000.00', 431 | '12000.00', 432 | '2023-08-18 23:59:59', 433 | '2023-12-05 23:59:59', 434 | '2023-08-02 09:45:00', 435 | '2023-08-02 09:45:00' 436 | ), 437 | ( 438 | '9c804cfc-1d8c-45de-9044-5948218b7a1e', 439 | 'Jazz Music Album', 440 | 'Soulful jazz music album', 441 | 'Recording an album featuring soulful jazz melodies and improvisations.', 442 | 'https://loremflickr.com/1000/640/jazz_music', 443 | 'ea22c5ed-3cf6-4ff1-9e32-9f2f39f32c94', 444 | 6000.00, 445 | 80, 446 | '5000.00', 447 | '5000.00', 448 | '8000.00', 449 | '2023-08-18 23:59:59', 450 | '2023-11-05 23:59:59', 451 | '2023-07-22 14:30:00', 452 | '2023-07-22 14:30:00' 453 | ); 454 | 455 | INSERT INTO "storage"."buckets" (id, name, public, avif_autodetection) 456 | VALUES ('projects', 'projects', true, false); 457 | --------------------------------------------------------------------------------