├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── adapters ├── baseAdapter.ts ├── other-apps │ ├── api │ │ └── adapter.ts │ ├── baseHttpAdapter.ts │ ├── caprover │ │ └── caproverAdapter.ts │ ├── digital-ocean │ │ └── digitalOceanAdapter.ts │ ├── dreamhost │ │ └── dreamhostAdapter.ts │ └── mastodon │ │ └── adapter.ts ├── tableDictionary.ts ├── user │ └── adapter.ts ├── userApps │ └── adapter.ts ├── userDroplets │ └── adapter.ts └── userPaas │ └── adapter.ts ├── assets ├── Logo.svg ├── base.css ├── chrome-bug.css ├── components.css └── main.css ├── components ├── Appsmith.js ├── ChatwootWidget.js ├── Comments.js ├── Layout.tsx ├── Pricing.js ├── Toaster │ ├── Toast.tsx │ └── Toaster.tsx ├── context │ ├── ToastContext.tsx │ └── newFlow │ │ ├── newFlowContext.tsx │ │ ├── newFlowReducer.tsx │ │ └── newFlowService.tsx ├── hooks │ ├── useDebounce.tsx │ ├── useDigitalOcean.tsx │ ├── useForm.tsx │ ├── useInput.tsx │ └── useToast.tsx ├── icons │ ├── Cross.tsx │ ├── Facebook.jsx │ ├── Gear.tsx │ ├── GitHub.jsx │ ├── Heart.jsx │ ├── Instagram.jsx │ ├── Logo.jsx │ ├── Pinterest.jsx │ ├── Plus.jsx │ ├── ThumbsUpFilled.jsx │ ├── ThumbsUpOutlined.jsx │ ├── TikTok.jsx │ ├── Twitter.jsx │ ├── User.jsx │ └── settings 1.svg ├── modals │ ├── installTool │ │ └── installToolModal.js │ ├── modalOverlay.tsx │ ├── modals.tsx │ ├── modalsContext.tsx │ ├── newFlow │ │ ├── newFlowInputInput.tsx │ │ ├── newFlowOutputInput.tsx │ │ └── newFlowStepInput.tsx │ ├── newFlowModal.tsx │ ├── newItem │ │ └── newItemModal.js │ └── search │ │ └── searchModal.tsx ├── scaleableImage.tsx └── ui │ ├── Button │ ├── Button.module.css │ ├── Button.tsx │ └── index.js │ ├── Footer │ └── index.js │ ├── Hero │ └── index.js │ ├── ImageInput.tsx │ ├── ImageMap.tsx │ ├── Input │ ├── Input.js │ ├── Input.module.css │ └── index.js │ ├── ListItem │ └── index.js │ ├── ListItemMirrored │ └── index.js │ ├── LoadingDots │ ├── LoadingDots.js │ ├── LoadingDots.module.css │ └── index.js │ ├── Navbar │ ├── Navbar.js │ ├── Navbar.module.css │ └── index.js │ ├── ParagraphWithButton │ └── index.js │ ├── PrettyBlock │ └── index.js │ ├── SearchBar │ └── SearchBar.tsx │ ├── SearchModal │ └── SearchModal.tsx │ ├── SectionsAndCategories │ └── index.js │ ├── Sidebar │ └── index.tsx │ ├── SquareBlock │ └── index.js │ ├── SquareCard │ └── index.js │ ├── TextList │ └── index.js │ ├── Title │ └── index.js │ └── comments │ ├── Avatar.tsx │ ├── Comment.tsx │ ├── CommentSection.tsx │ ├── CommentSkeleton.tsx │ ├── CommentsList.tsx │ ├── NewCommentForm.tsx │ ├── NewUserModal.tsx │ ├── SignInModal.tsx │ ├── SortCommentsSelect.tsx │ └── VoteButtons.tsx ├── core └── errors.ts ├── jsconfig.json ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.js ├── account.js ├── action │ └── [action_id].js ├── api │ ├── caprover │ │ ├── change-root-domain.ts │ │ ├── enable-ssl.ts │ │ ├── force-ssl.ts │ │ └── login.ts │ ├── create-checkout-session.js │ ├── create-portal-link.js │ ├── dreamhost │ │ └── add-dns.ts │ ├── ocean-auth.ts │ └── webhooks.js ├── apps.js ├── articles │ └── [article_id].tsx ├── flow │ └── [flow_id].tsx ├── flows.js ├── index.js ├── listings.js ├── myflows │ └── index.tsx ├── new-flow.tsx ├── new-paas.tsx ├── output │ └── [output_id].js ├── privacy.js ├── recovery.tsx ├── search.tsx ├── section │ └── [section_title].js ├── signin.tsx ├── signup.js ├── task │ └── [task_id].js ├── tasks.js ├── terms.js ├── tool │ └── [tool_id].js ├── tools.js └── usecases.js ├── postcss.config.js ├── public ├── OW_favicon.png ├── architecture_diagram.svg ├── demo.png ├── github.svg ├── nextjs.svg ├── og.png ├── stripe.svg ├── supabase.svg ├── vercel-deploy.png ├── vercel.svg └── zoom.js ├── schema.sql ├── services └── validationService.ts ├── tailwind.config.js ├── tsconfig.json ├── types └── supabase.ts ├── utils ├── autosize.ts ├── getFlagUrl.ts ├── getRandomGradient.js ├── helpers.js ├── pagination.ts ├── regex │ ├── punctuationRegex.js │ └── validateEmailRegex.js ├── stripe-client.js ├── stripe.js ├── supabase-admin.js ├── supabase-client.ts ├── types.ts ├── use-comments.tsx ├── use-modal.tsx ├── use-user.tsx ├── useDatabase.js └── useUser.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | 5 | Logo 6 | 7 | 8 |

Gardens

9 | 10 |
11 | Vercel badge 12 | SonarCloud security rating 13 | SonarCloud maintainability 14 |
15 |
16 | SonarCloud technical debt 17 | Last commit 18 | Mastodon community
19 | 20 | 21 | --- 22 | 23 |
24 |

25 | A platform to discover and write guides/work processes 26 |
27 | This README is a work in progress. Stay tuned for more! 28 |

29 |
30 |

31 | 32 | 33 | -------------------------------------------------------------------------------- /adapters/baseAdapter.ts: -------------------------------------------------------------------------------- 1 | import { createClient, SupabaseClient } from "@supabase/supabase-js"; 2 | import { BigNumber } from "bignumber.js" 3 | 4 | export type SupabaseInsertionDoc = Omit, "created_at"> 5 | 6 | export class BaseAdapter { 7 | readonly supabase: SupabaseClient 8 | constructor( 9 | private tableName: string 10 | ) { 11 | this.supabase = createClient( 12 | process.env.NEXT_PUBLIC_SUPABASE_URL, 13 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY 14 | ); 15 | } 16 | 17 | async insert(entities: Partial[]) { 18 | const response = await this.supabase 19 | .from(this.tableName) 20 | .insert(entities) 21 | return response 22 | } 23 | 24 | async insertOne(entity: Partial) { 25 | const response = await this.insert([entity]) 26 | if (response.data) { 27 | return response.data[0] 28 | } 29 | if (response.error) { 30 | return false 31 | } 32 | } 33 | 34 | async findManyByQuery(query: Partial, page = 1, limit = 10) { 35 | let response = this.supabase 36 | .from(this.tableName) 37 | .select('*') 38 | const first = +new BigNumber(page).minus(1).multipliedBy(limit) 39 | const second = +new BigNumber(page).multipliedBy(limit).minus(1) 40 | for (let key of Object.entries(query)) { 41 | 42 | response = response.eq(key[0] as keyof T, key[1] as T[keyof T]) 43 | } 44 | response.limit(limit) 45 | console.log(first, second) 46 | response.range(first, second) 47 | return await response 48 | } 49 | 50 | async findOneByQuery(query: Partial) { 51 | let response = await this.findManyByQuery(query, 1, 1) 52 | return response 53 | } 54 | 55 | 56 | } -------------------------------------------------------------------------------- /adapters/other-apps/api/adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseHttpAdapter } from "../baseHttpAdapter"; 2 | 3 | type DigitalOceanResponseData = { 4 | token: string 5 | } 6 | 7 | class ApiAdapter extends BaseHttpAdapter { 8 | constructor() { 9 | const url = process.env.NODE_ENV === "development" ? "http://localhost:3000/" : "https://www.joingardens.com/" 10 | super(url) 11 | } 12 | 13 | async getDigitalOceanCode(code: string) { 14 | const response = await this.instance.post("/api/ocean-auth", {code}) 15 | return response 16 | } 17 | } 18 | 19 | export class DreamhostServerAdapter extends ApiAdapter { 20 | constructor(private ip: string) { 21 | super() 22 | } 23 | 24 | async addDnsRecord(domain: string) { 25 | const response = await this.instance.post("api/dreamhost/add-dns", { 26 | domain, 27 | ip: this.ip 28 | }) 29 | return response 30 | } 31 | 32 | } 33 | 34 | export class CaproverServerApiAdapter extends ApiAdapter { 35 | private token: string 36 | private url: string 37 | constructor (url: string) { 38 | super() 39 | this.url = `http://${url}:3000` 40 | } 41 | 42 | setToken(token: string) { 43 | this.token = token 44 | return this 45 | } 46 | 47 | async caproverLogin() { 48 | const response = await this.instance.post<{token: string}>("api/caprover/login", { 49 | url: this.url 50 | }) 51 | return response 52 | } 53 | 54 | async caproverSetRootDomain(domain: string) { 55 | const response = await this.instance.post("api/caprover/change-root-domain", { 56 | url: this.url, 57 | token: this.token, 58 | domain 59 | }) 60 | return response 61 | } 62 | 63 | async caproverEnableSSL(email: string) { 64 | const response = await this.instance.post("api/caprover/enable-ssl", { 65 | url: this.url, 66 | token: this.token, 67 | email 68 | }) 69 | return response 70 | } 71 | 72 | async caproverForceSSL() { 73 | const response = await this.instance.post("api/caprover/force-ssl", { 74 | url: this.url, 75 | token: this.token 76 | }) 77 | return response 78 | } 79 | 80 | } 81 | 82 | export const apiAdapter = new ApiAdapter() -------------------------------------------------------------------------------- /adapters/other-apps/baseHttpAdapter.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios" 2 | 3 | export class BaseHttpAdapter { 4 | instance: AxiosInstance 5 | constructor( baseURL: string ) { 6 | this.instance = axios.create({ 7 | baseURL 8 | }) 9 | } 10 | 11 | 12 | } -------------------------------------------------------------------------------- /adapters/other-apps/caprover/caproverAdapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseHttpAdapter } from "../baseHttpAdapter"; 2 | 3 | class CaproverResponse { 4 | status: number 5 | description: string 6 | data: T 7 | } 8 | 9 | class CaproverTokenData { 10 | token: string 11 | } 12 | 13 | export class CaproverAdapter extends BaseHttpAdapter { 14 | constructor(url: string) { 15 | super(url) 16 | this.instance.defaults.headers["x-namespace"] = "captain" 17 | } 18 | 19 | setToken(token: string) { 20 | this.instance.defaults.headers["x-captain-auth"] = token 21 | return this 22 | } 23 | 24 | async login(password?: string) { 25 | const response = await this.instance.post>("/api/v2/login/", { 26 | "password": password ? password : "captain42" 27 | }) 28 | return response 29 | } 30 | 31 | async changeRootDomain(domain: string) { 32 | const response = await this.instance.post>("/api/v2/user/system/changerootdomain", { 33 | rootDomain: domain 34 | }) 35 | return response 36 | } 37 | 38 | async enableSsl(email: string) { 39 | const response = await this.instance.post>("/api/v2/user/system/enablessl", { 40 | emailAddress: email 41 | }) 42 | return response 43 | } 44 | 45 | async forceSsl(bool: boolean) { 46 | const response = await this.instance.post>("/api/v2/user/system/forcessl", { 47 | isEnabled: bool 48 | }) 49 | return response 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /adapters/other-apps/digital-ocean/digitalOceanAdapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from "../../baseAdapter"; 2 | import { BaseHttpAdapter } from "../baseHttpAdapter"; 3 | 4 | export class DigitalOceanRegion { 5 | name: string 6 | slug: string 7 | sizes: string[] 8 | available: boolean 9 | } 10 | 11 | export class DigitalOceanRegionsResponse { 12 | links: any 13 | meta: {total: number} 14 | regions: DigitalOceanRegion[] 15 | } 16 | 17 | export class DigitalOceanDroplet { 18 | id: number; 19 | name: string; 20 | networks: { 21 | "v4": DigitalOceanNetworkType[] 22 | } 23 | } 24 | 25 | export class DigitalOceanNetworkType { 26 | "ip_address": string 27 | "netmask": string 28 | "gateway": string 29 | "type": "public" | "private" 30 | } 31 | 32 | export class DigitalOceanDropletResponse { 33 | droplet: DigitalOceanDroplet 34 | } 35 | 36 | class DigitalOceanApiAdapter extends BaseHttpAdapter { 37 | constructor() { 38 | super("https://api.digitalocean.com/v2/") 39 | } 40 | 41 | setToken(token: string) { 42 | console.log("token changed") 43 | this.instance.defaults.headers.common['Authorization'] = `Bearer ${token}`; 44 | } 45 | 46 | async getRegions() { 47 | const response = await this.instance.get("regions") 48 | const filteredResp = response.data.regions.filter(a => a.available) 49 | return filteredResp 50 | } 51 | 52 | async createDroplet(name: string, region: string, size: string) { 53 | const response = await this.instance.post("droplets", { 54 | name, 55 | region, 56 | size, 57 | image: "caprover-18-04" 58 | }) 59 | return response 60 | } 61 | 62 | async getDroplet(id: number) { 63 | const response = await this.instance.get(`droplets/${id}`) 64 | return response 65 | } 66 | } 67 | 68 | export const digitalOceanApiAdapter = new DigitalOceanApiAdapter() -------------------------------------------------------------------------------- /adapters/other-apps/dreamhost/dreamhostAdapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseHttpAdapter } from "../baseHttpAdapter"; 2 | 3 | class DreamhostAdapter extends BaseHttpAdapter { 4 | constructor() { 5 | super("https://api.dreamhost.com/") 6 | this.instance.interceptors.request.use(function (config) { 7 | config.params = { 8 | key: process.env.DREAMHOST_API_KEY, 9 | ...config.params 10 | } 11 | return config; 12 | }, function (error) { 13 | // Do something with request error 14 | return Promise.reject(error); 15 | }) 16 | } 17 | 18 | async addDnsRecord (domain: string, ip: string) { 19 | const result = await this.instance.get("", { 20 | params: { 21 | cmd: "dns-add_record", 22 | type: "A", 23 | value: ip, 24 | record: domain 25 | } 26 | }) 27 | return result.data 28 | } 29 | 30 | async removeDnsRecord (domain: string, ip: string) { 31 | const result = await this.instance.get("", { 32 | params: { 33 | cmd: "dns-remove_record", 34 | type: "A", 35 | value: ip, 36 | record: domain 37 | } 38 | }) 39 | return result.data 40 | } 41 | 42 | async listDnsRecords() { 43 | const result = await this.instance.get("", { 44 | params: { 45 | cmd: "dns-list_records", 46 | } 47 | }) 48 | return result.data 49 | } 50 | } 51 | 52 | export const dreamhostAdapter = new DreamhostAdapter() -------------------------------------------------------------------------------- /adapters/other-apps/mastodon/adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseHttpAdapter } from "../../other-apps/baseHttpAdapter"; 2 | 3 | class MastodonApiAdapter extends BaseHttpAdapter { 4 | constructor() { 5 | const url = "https://masto.cloud.joingardens.com/api/v1/" 6 | super(url) 7 | } 8 | 9 | async getRepliesByStatusId(statusId: string) { 10 | const response = await this.instance.get(("/statuses/" + statusId + "/context")).catch((error) => { 11 | console.log(error) 12 | }) 13 | if (response){ 14 | return response 15 | } 16 | } 17 | 18 | async getRecentMastoPosts() { 19 | const response = await this.instance.get(("/timelines/public?limit=5")).catch((error) => { 20 | console.log(error) 21 | }) 22 | if (response){ 23 | return response 24 | } 25 | } 26 | 27 | async getStatusById(statusId: string) { 28 | const response = await this.instance.get(("/statuses/" + statusId)).catch((error) => { 29 | console.log(error) 30 | }) 31 | if (response){ 32 | return response 33 | } 34 | } 35 | 36 | async postReply(replyToId: string) { 37 | const response = await this.instance.post("/statuses/", { 38 | 39 | }) 40 | return response 41 | } 42 | } 43 | 44 | export const mastodonAdapter = new MastodonApiAdapter() -------------------------------------------------------------------------------- /adapters/tableDictionary.ts: -------------------------------------------------------------------------------- 1 | export enum TableDictionary { 2 | USER_APPS = "user_apps", 3 | USER_PAAS = "user_paas", 4 | USERS="users", 5 | USER_DROPLETS="user_droplets" 6 | } -------------------------------------------------------------------------------- /adapters/user/adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from "../baseAdapter"; 2 | import { parameters } from "../../types/supabase" 3 | import { TableDictionary } from "../tableDictionary"; 4 | 5 | class UsersAdapter extends BaseAdapter { 6 | constructor() { 7 | super(TableDictionary.USERS) 8 | } 9 | 10 | async sendRecoveryRequest( email: string ) { 11 | const response = await this.supabase.auth.api.resetPasswordForEmail(email) 12 | return response 13 | } 14 | 15 | async recoverPassword( accessToken: string, password: string) { 16 | const response = await this.supabase.auth.api.updateUser(accessToken, { password }) 17 | return response 18 | } 19 | } 20 | 21 | export const userAdapter = new UsersAdapter() -------------------------------------------------------------------------------- /adapters/userApps/adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from "../baseAdapter"; 2 | import { parameters } from "../../types/supabase" 3 | import { TableDictionary } from "../tableDictionary"; 4 | 5 | class UserAppsAdapter extends BaseAdapter { 6 | constructor() { 7 | super(TableDictionary.USER_APPS) 8 | } 9 | } 10 | 11 | export const userAppsAdapter = new UserAppsAdapter() -------------------------------------------------------------------------------- /adapters/userDroplets/adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from "../baseAdapter"; 2 | import { parameters } from "../../types/supabase" 3 | import { TableDictionary } from "../tableDictionary"; 4 | 5 | class UserDropletsAdapter extends BaseAdapter { 6 | constructor() { 7 | super(TableDictionary.USER_DROPLETS) 8 | } 9 | } 10 | 11 | export const userDropletsAdapter = new UserDropletsAdapter() -------------------------------------------------------------------------------- /adapters/userPaas/adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from "../baseAdapter"; 2 | import { parameters } from "../../types/supabase" 3 | import { TableDictionary } from "../tableDictionary"; 4 | 5 | class UserPaasAdapter extends BaseAdapter { 6 | constructor() { 7 | super(TableDictionary.USER_PAAS) 8 | } 9 | } 10 | 11 | export const userPaasAdapter = new UserPaasAdapter() -------------------------------------------------------------------------------- /assets/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #fff; 3 | --primary-2: #432818; 4 | --secondary: #000; 5 | --secondary-2: #212529; 6 | --hover: rgba(0, 0, 0, 0.7); 7 | --hover-1: rgba(0, 0, 0, 0.5); 8 | --hover-2: rgba(0, 0, 0, 0.4); 9 | --selection: var(--purple); 10 | 11 | --text-base: #212529; 12 | --text-primary: #212529; 13 | --text-secondary: #fff; 14 | 15 | --accents-0: #000; 16 | --accents-1: #000; 17 | --accents-2: #000; 18 | --accents-3: #000; 19 | --accents-4: #fff; 20 | --accents-5: #000; 21 | --accents-6: #000; 22 | --accents-7: #000; 23 | --accents-8: #000; 24 | --accents-9: #000; 25 | 26 | 27 | --font-sans: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue', 28 | 'Helvetica', sans-serif; 29 | } 30 | 31 | *, 32 | *:before, 33 | *:after { 34 | box-sizing: inherit; 35 | } 36 | 37 | *:focus { 38 | @apply outline-none border; 39 | } 40 | 41 | html { 42 | height: 100%; 43 | box-sizing: border-box; 44 | touch-action: manipulation; 45 | font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0; 46 | text-rendering: optimizeLegibility; 47 | -webkit-font-smoothing: antialiased; 48 | -moz-osx-font-smoothing: grayscale; 49 | } 50 | 51 | html, 52 | body { 53 | font-family: var(--font-sans); 54 | text-rendering: optimizeLegibility; 55 | -webkit-font-smoothing: antialiased; 56 | -moz-osx-font-smoothing: grayscale; 57 | color: var(--text-primary); 58 | } 59 | 60 | body { 61 | position: relative; 62 | min-height: 100%; 63 | margin: 0; 64 | } 65 | 66 | a { 67 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 68 | } 69 | 70 | .ce-header { 71 | @apply text-2xl font-semibold; 72 | } 73 | 74 | .ce-block a { 75 | @apply text-blue-700 font-semibold; 76 | } 77 | 78 | .animated { 79 | -webkit-animation-duration: 1s; 80 | animation-duration: 1s; 81 | -webkit-animation-duration: 1s; 82 | animation-duration: 1s; 83 | -webkit-animation-fill-mode: both; 84 | animation-fill-mode: both; 85 | } 86 | 87 | .fadeIn { 88 | -webkit-animation-name: fadeIn; 89 | animation-name: fadeIn; 90 | } 91 | 92 | .tooltip { 93 | @apply invisible absolute; 94 | } 95 | 96 | .has-tooltip:hover .tooltip { 97 | @apply visible z-50; 98 | } 99 | 100 | 101 | @-webkit-keyframes fadeIn { 102 | from { 103 | opacity: 0; 104 | } 105 | 106 | to { 107 | opacity: 1; 108 | } 109 | } 110 | 111 | @keyframes fadeIn { 112 | from { 113 | opacity: 0; 114 | } 115 | 116 | to { 117 | opacity: 1; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /assets/chrome-bug.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Chrome has a bug with transitions on load since 2012! 3 | * 4 | * To prevent a "pop" of content, you have to disable all transitions until 5 | * the page is done loading. 6 | * 7 | * https://lab.laukstein.com/bug/input 8 | * https://twitter.com/timer150/status/1345217126680899584 9 | */ 10 | body.loading * { 11 | transition: none !important; 12 | } 13 | -------------------------------------------------------------------------------- /assets/components.css: -------------------------------------------------------------------------------- 1 | .fit { 2 | min-height: calc(100vh - 88px); 3 | } 4 | -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @import './base.css'; 3 | 4 | @tailwind components; 5 | @import './components.css'; 6 | 7 | @tailwind utilities; 8 | -------------------------------------------------------------------------------- /components/Appsmith.js: -------------------------------------------------------------------------------- 1 | export default function Appsmith(props) { 2 | 3 | return ( 4 | <> 5 |
6 | 7 |
8 | 9 | ) 10 | } 11 | 12 | -------------------------------------------------------------------------------- /components/ChatwootWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | class ChatwootWidget extends React.Component { 3 | 4 | 5 | componentDidMount () { 6 | 7 | // Add Chatwoot Settings 8 | window.chatwootSettings = { 9 | hideMessageBubble: false, 10 | position: 'right', // This can be left or right 11 | locale: 'en', // Language to be set 12 | type: 'standard', // [standard, expanded_bubble] 13 | }; 14 | 15 | // Paste the script from inbox settings except the 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | 50 | export default MyApp -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | ); 14 | } 15 | } 16 | 17 | export default MyDocument; 18 | -------------------------------------------------------------------------------- /pages/account.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useRouter } from 'next/router'; 3 | import { useState, useEffect } from 'react'; 4 | import LoadingDots from '../components/ui/LoadingDots'; 5 | import Button from '../components/ui/Button'; 6 | import { useUser } from '../utils/useUser'; 7 | import { postData } from '../utils/helpers'; 8 | 9 | function Card({ title, description, footer, children }) { 10 | return ( 11 |
12 |
13 |

{title}

14 |

{description}

15 | {children} 16 |
17 |
18 | {footer} 19 |
20 |
21 | ); 22 | } 23 | export default function Account() { 24 | const [loading, setLoading] = useState(false); 25 | const router = useRouter(); 26 | const { userLoaded, user, session, userDetails, subscription } = useUser(); 27 | 28 | useEffect(() => { 29 | if (!user) router.replace('/signin'); 30 | }, [user]); 31 | 32 | const redirectToCustomerPortal = async () => { 33 | setLoading(true); 34 | const { url, error } = await postData({ 35 | url: '/api/create-portal-link', 36 | token: session.access_token 37 | }); 38 | if (error) return alert(error.message); 39 | window.location.assign(url); 40 | setLoading(false); 41 | }; 42 | 43 | const subscriptionName = subscription && subscription.prices.products.name; 44 | const subscriptionPrice = 45 | subscription && 46 | new Intl.NumberFormat('en-US', { 47 | style: 'currency', 48 | currency: subscription.prices.currency, 49 | minimumFractionDigits: 0 50 | }).format(subscription.prices.unit_amount / 100); 51 | 52 | return ( 53 |
54 |
55 |
56 |

57 | Account 58 |

59 |

60 | We partnered with Stripe for a simplified billing. 61 |

62 |
63 |
64 |
65 | 73 |

74 | Manage your subscription on Stripe. 75 |

76 | 84 |
85 | } 86 | > 87 |
88 | {!userLoaded ? ( 89 |
90 | 91 |
92 | ) : subscriptionPrice ? ( 93 | `${subscriptionPrice}/${subscription.prices.interval}` 94 | ) : ( 95 | 96 | Choose your plan 97 | 98 | )} 99 |
100 | 101 | Please use 64 characters at maximum.

} 105 | > 106 |
107 | {userDetails ? ( 108 | `${userDetails?.full_name ?? ''}` 109 | ) : ( 110 |
111 | 112 |
113 | )} 114 |
115 |
116 | We will email you to verify the change.

} 120 | > 121 |

122 | {user ? user.email : undefined} 123 |

124 |
125 | 126 |
127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /pages/action/[action_id].js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRouter } from 'next/router' 3 | import Link from 'next/link'; 4 | import Pricing from '../../components/Pricing'; 5 | import Title from '../../components/ui/Title'; 6 | import { getAllActionIds, getActionById } from '../../utils/supabase-client'; 7 | import Image from 'next/image'; 8 | import Appsmith from '../../components/Appsmith'; 9 | 10 | export default function Action({ action }) { 11 | 12 | const router = useRouter() 13 | if (router.isFallback) { 14 | return (
15 |

Nothing here... 16 | Go home?

17 |
18 | ) 19 | } else { 20 | const { action_id } = router.query; 21 | 22 | const currentAction = action[0]; 23 | const generatedTitle = currentAction.action; 24 | const generatedDescription = currentAction.description; 25 | 26 | return ( 27 | <> 28 |
29 | 31 | </div> 32 | <div className="flex mt-24 pt-10 w-full h-full mx-auto items-center px-6 lg:px-12"> 33 | <Appsmith 34 | appsrc={currentAction.appsrc} /> 35 | </div> 36 | </> 37 | )} 38 | } 39 | 40 | export async function getStaticPaths() { 41 | const allActionIds = await getAllActionIds(); 42 | let actionPaths = []; 43 | let actionIds = allActionIds.map(actionId => 44 | { 45 | if (!actionId.isInternal){ 46 | actionPaths.push({params: {action_id: actionId.id.toString()}}) 47 | } 48 | }) 49 | 50 | return { 51 | paths: actionPaths, 52 | fallback: true, 53 | } 54 | } 55 | 56 | export async function getStaticProps(context) { 57 | 58 | // user = await getPersonalDetailsByUserId(???); 59 | const allActions = await getAllActionIds(); 60 | const action = await getActionById(context.params.action_id); 61 | 62 | if (!action) { 63 | return { 64 | notFound: true, 65 | } 66 | } 67 | 68 | return { 69 | props: { 70 | allActions, 71 | action 72 | }, 73 | revalidate: 60 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /pages/api/caprover/change-root-domain.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next" 2 | import { CaproverAdapter } from "../../../adapters/other-apps/caprover/caproverAdapter" 3 | 4 | export default async (req: NextApiRequest, res : NextApiResponse) => { 5 | if (req.method === "POST") { 6 | const token = req.body.token 7 | const url = req.body.url 8 | const domain = req.body.domain 9 | const adapter = new CaproverAdapter(url).setToken(token) 10 | const response = await adapter.changeRootDomain(domain) 11 | if (response.data.status > 100) { 12 | return res.status(500).json(response.data.description) 13 | } 14 | return res.status(201).json(response.data) 15 | } 16 | } -------------------------------------------------------------------------------- /pages/api/caprover/enable-ssl.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next" 2 | import { CaproverAdapter } from "../../../adapters/other-apps/caprover/caproverAdapter" 3 | 4 | export default async (req: NextApiRequest, res : NextApiResponse) => { 5 | if (req.method === "POST") { 6 | const token = req.body.token 7 | const url = req.body.url 8 | const email = req.body.email 9 | const adapter = new CaproverAdapter(url).setToken(token) 10 | const response = await adapter.enableSsl(email) 11 | return res.status(201).json(response.data) 12 | } 13 | } -------------------------------------------------------------------------------- /pages/api/caprover/force-ssl.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next" 2 | import { CaproverAdapter } from "../../../adapters/other-apps/caprover/caproverAdapter" 3 | 4 | export default async (req: NextApiRequest, res : NextApiResponse) => { 5 | if (req.method === "POST") { 6 | const token = req.body.token 7 | const url = req.body.url 8 | const adapter = new CaproverAdapter(url).setToken(token) 9 | const response = await adapter.forceSsl(true) 10 | return res.status(201).json(response.data) 11 | } 12 | } -------------------------------------------------------------------------------- /pages/api/caprover/login.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { NextApiRequest, NextApiResponse } from "next" 3 | import { CaproverAdapter } from "../../../adapters/other-apps/caprover/caproverAdapter" 4 | 5 | export default async (req: NextApiRequest, res : NextApiResponse) => { 6 | if (req.method === "POST") { 7 | const password = req.body.password ? req.body.password : "" 8 | const adapter = new CaproverAdapter(req.body.url) 9 | try { 10 | const response = await adapter.login(password) 11 | return res.status(200).json({token: response.data.data.token}) 12 | } 13 | catch(e) { 14 | return res.status(500) 15 | } 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /pages/api/create-checkout-session.js: -------------------------------------------------------------------------------- 1 | import { stripe } from '../../utils/stripe'; 2 | import { getUser } from '../../utils/supabase-admin'; 3 | import { createOrRetrieveCustomer } from '../../utils/useDatabase'; 4 | import { getURL } from '../../utils/helpers'; 5 | 6 | const createCheckoutSession = async (req, res) => { 7 | if (req.method === 'POST') { 8 | const token = req.headers.token; 9 | const { price, quantity = 1, metadata = {} } = req.body; 10 | 11 | try { 12 | const user = await getUser(token); 13 | const customer = await createOrRetrieveCustomer({ 14 | uuid: user.id, 15 | email: user.email 16 | }); 17 | 18 | const session = await stripe.checkout.sessions.create({ 19 | payment_method_types: ['card'], 20 | billing_address_collection: 'required', 21 | customer, 22 | line_items: [ 23 | { 24 | price, 25 | quantity 26 | } 27 | ], 28 | mode: 'subscription', 29 | allow_promotion_codes: true, 30 | subscription_data: { 31 | trial_from_plan: true, 32 | metadata 33 | }, 34 | success_url: `${getURL()}/account`, 35 | cancel_url: `${getURL()}/` 36 | }); 37 | 38 | return res.status(200).json({ sessionId: session.id }); 39 | } catch (err) { 40 | console.log(err); 41 | res 42 | .status(500) 43 | .json({ error: { statusCode: 500, message: err.message } }); 44 | } 45 | } else { 46 | res.setHeader('Allow', 'POST'); 47 | res.status(405).end('Method Not Allowed'); 48 | } 49 | }; 50 | 51 | export default createCheckoutSession; 52 | -------------------------------------------------------------------------------- /pages/api/create-portal-link.js: -------------------------------------------------------------------------------- 1 | import { stripe } from '../../utils/stripe'; 2 | import { getUser } from '../../utils/supabase-admin'; 3 | import { createOrRetrieveCustomer } from '../../utils/useDatabase'; 4 | import { getURL } from '../../utils/helpers'; 5 | 6 | const createPortalLink = async (req, res) => { 7 | if (req.method === 'POST') { 8 | const token = req.headers.token; 9 | 10 | try { 11 | const user = await getUser(token); 12 | const customer = await createOrRetrieveCustomer({ 13 | uuid: user.id, 14 | email: user.email 15 | }); 16 | 17 | const { url } = await stripe.billingPortal.sessions.create({ 18 | customer, 19 | return_url: `${getURL()}/account` 20 | }); 21 | 22 | return res.status(200).json({ url }); 23 | } catch (err) { 24 | console.log(err); 25 | res 26 | .status(500) 27 | .json({ error: { statusCode: 500, message: err.message } }); 28 | } 29 | } else { 30 | res.setHeader('Allow', 'POST'); 31 | res.status(405).end('Method Not Allowed'); 32 | } 33 | }; 34 | 35 | export default createPortalLink; 36 | -------------------------------------------------------------------------------- /pages/api/dreamhost/add-dns.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next" 2 | import { dreamhostAdapter } from "../../../adapters/other-apps/dreamhost/dreamhostAdapter" 3 | 4 | export default async (req: NextApiRequest, res : NextApiResponse) => { 5 | if (req.method === "POST") { 6 | const ip = req.body.ip 7 | const domain = req.body.domain 8 | const adapter = dreamhostAdapter 9 | const removal = await adapter.removeDnsRecord(domain, ip) 10 | console.log(removal) 11 | const response = await adapter.addDnsRecord(domain, ip) 12 | console.log(response) 13 | if (response.split("\n")[0] === "error") { 14 | return res.status(500).json({}) 15 | } 16 | console.log(ip,domain) 17 | console.log(response) 18 | return res.status(200).json({}) 19 | } 20 | } -------------------------------------------------------------------------------- /pages/api/ocean-auth.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { NextApiRequest, NextApiResponse } from "next" 3 | 4 | export default async (req: NextApiRequest, res : NextApiResponse) => { 5 | if (req.method === "POST") { 6 | const response = await axios.post("https://cloud.digitalocean.com/v1/oauth/token", {}, { 7 | params: { 8 | code: req.body.code as string, 9 | client_id: process.env.NEXT_PUBLIC_DIGITAL_OCEAN_CLIENT_ID, 10 | client_secret: process.env.DIGITAL_OCEAN_SECRET, 11 | redirect_uri: "https://joingardens.com/onboarding/provision", 12 | grant_type: "authorization_code" 13 | } 14 | }) 15 | console.log(req.body.code) 16 | if (response.data) { 17 | console.log(response.data) 18 | return res.status(201).json({token: response.data.access_token}) 19 | } 20 | return res.status(500).json({}) 21 | 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /pages/api/webhooks.js: -------------------------------------------------------------------------------- 1 | import { stripe } from '../../utils/stripe'; 2 | import { 3 | upsertProductRecord, 4 | upsertPriceRecord, 5 | manageSubscriptionStatusChange 6 | } from '../../utils/useDatabase'; 7 | 8 | // Stripe requires the raw body to construct the event. 9 | export const config = { 10 | api: { 11 | bodyParser: false 12 | } 13 | }; 14 | 15 | async function buffer(readable) { 16 | const chunks = []; 17 | for await (const chunk of readable) { 18 | chunks.push( 19 | typeof chunk === "string" ? Buffer.from(chunk) : chunk 20 | ); 21 | } 22 | return Buffer.concat(chunks); 23 | } 24 | 25 | const relevantEvents = new Set([ 26 | 'product.created', 27 | 'product.updated', 28 | 'price.created', 29 | 'price.updated', 30 | 'checkout.session.completed', 31 | 'customer.subscription.created', 32 | 'customer.subscription.updated', 33 | 'customer.subscription.deleted' 34 | ]); 35 | 36 | const webhookHandler = async (req, res) => { 37 | if (req.method === 'POST') { 38 | const buf = await buffer(req); 39 | const sig = req.headers['stripe-signature']; 40 | const webhookSecret = 41 | process.env.STRIPE_WEBHOOK_SECRET_LIVE ?? 42 | process.env.STRIPE_WEBHOOK_SECRET; 43 | let event; 44 | 45 | try { 46 | event = stripe.webhooks.constructEvent(buf, sig, webhookSecret); 47 | } catch (err) { 48 | console.log(`❌ Error message: ${err.message}`); 49 | return res.status(400).send(`Webhook Error: ${err.message}`); 50 | } 51 | 52 | if (relevantEvents.has(event.type)) { 53 | try { 54 | switch (event.type) { 55 | case 'product.created': 56 | case 'product.updated': 57 | await upsertProductRecord(event.data.object); 58 | break; 59 | case 'price.created': 60 | case 'price.updated': 61 | await upsertPriceRecord(event.data.object); 62 | break; 63 | case 'customer.subscription.created': 64 | case 'customer.subscription.updated': 65 | case 'customer.subscription.deleted': 66 | await manageSubscriptionStatusChange( 67 | event.data.object.id, 68 | event.data.object.customer, 69 | event.type === 'customer.subscription.created' 70 | ); 71 | break; 72 | case 'checkout.session.completed': 73 | const checkoutSession = event.data.object; 74 | if (checkoutSession.mode === 'subscription') { 75 | const subscriptionId = checkoutSession.subscription; 76 | await manageSubscriptionStatusChange( 77 | subscriptionId, 78 | checkoutSession.customer, 79 | true 80 | ); 81 | } 82 | break; 83 | default: 84 | throw new Error('Unhandled relevant event!'); 85 | } 86 | } catch (error) { 87 | console.log(error); 88 | return res.json({ error: 'Webhook handler failed. View logs.' }); 89 | } 90 | } 91 | 92 | res.json({ received: true }); 93 | } else { 94 | res.setHeader('Allow', 'POST'); 95 | res.status(405).end('Method Not Allowed'); 96 | } 97 | }; 98 | 99 | export default webhookHandler; 100 | -------------------------------------------------------------------------------- /pages/flows.js: -------------------------------------------------------------------------------- 1 | import LightHeroD from '../components/ui/Hero'; 2 | import ParagraphWithButton from '../components/ui/ParagraphWithButton'; 3 | import ListItem from '../components/ui/ListItem'; 4 | import TextList from '../components/ui/TextList'; 5 | import ListItemMirrored from '../components/ui/ListItemMirrored'; 6 | import Title from '../components/ui/Title'; 7 | import { getAllFlows, getAllFlowItems, getAllFlowItemsWithTools, getAllActions } from '../utils/supabase-client'; 8 | import SquareBlock from '../components/ui/SquareBlock'; 9 | import getRandomGradient from '../utils/getRandomGradient'; 10 | import PrettyBlock from '../components/ui/PrettyBlock'; 11 | 12 | export default function Flows({ flows, flowItemsWithTools }) { 13 | 14 | let groupArray = [] 15 | //const uniqueGroups = [...new Set(flows.map(flow => flow.job_group))]; 16 | 17 | const uniqueGroups = []; 18 | const map = new Map(); 19 | for (const flow of flows){ 20 | if(flow.job_group){ 21 | if(!map.has(flow.job_group.id)){ 22 | map.set(flow.job_group.id, true); 23 | uniqueGroups.push({ 24 | id: flow.job_group.id, 25 | job_group: flow.job_group.job_group 26 | }); 27 | }} 28 | }; 29 | 30 | const listFlows = uniqueGroups.map((group) => { 31 | let sortedItemArray = []; 32 | if (group){ 33 | sortedItemArray = flows.filter(item => { 34 | if ((item.job_group && (item.job_group.id == group.id))){ 35 | return item 36 | } 37 | }); 38 | } else { 39 | sortedItemArray = flows.filter(item => !item.job_group) 40 | } 41 | 42 | const itemElements = sortedItemArray.map(flow => { 43 | const currentFlowItems = flowItemsWithTools.filter(flowItem => flowItem.flow == flow.id); 44 | const allToolTitles = [...new Set(currentFlowItems.map(item => item.job_tool.tool.tool))]; 45 | const allToolImages = [...new Set(currentFlowItems.map(item => item.job_tool.tool.logo_url))]; 46 | return ( 47 | <PrettyBlock key={flow.id} smallImage={allToolImages[0] ? allToolImages[0] : null} 48 | blockLink={'/flow/' + flow.id} blockBody={flow.flow} 49 | flexibleHeight={true} fullWidth={true} 50 | blockDescription={`Using ${allToolTitles.toString().split(',').join(', ')}`} 51 | blockSubtitle={`By ${flow.user_public_profile ? (flow.user_public_profile.full_name) : null}`} /> 52 | ) 53 | } 54 | ); 55 | let currentGroupTitle = group ? group.job_group : 'General'; 56 | groupArray.push(currentGroupTitle); 57 | return ( 58 | <ListItem key={currentGroupTitle.toString()} categoryName={currentGroupTitle.toString()} emoji={group ? group.emoji : '📂'} categoryDescription={''}> 59 | {itemElements} 60 | </ListItem>) 61 | } 62 | ); 63 | 64 | 65 | return ( 66 | <> 67 | <div className="-mb-20 -mt-20"> 68 | <Title titleTitle={'Guides'} 69 | titleDescription={'All guides, sorted by category'} /> 70 | </div> 71 | <div className="flex justify-center"> 72 | <aside className="h-screen sticky top-0 w-1/5 hidden md:block"> 73 | <div className="pt-20 h-full"> 74 | <TextList items={groupArray} /> 75 | </div> 76 | </aside> 77 | <div className="flex flex-wrap px-5 w-full justify-start mb-24"> 78 | {listFlows} 79 | </div> 80 | </div> 81 | </> 82 | ) 83 | } 84 | 85 | export async function getStaticProps() { 86 | const flows = await getAllFlows(); 87 | const flowItemsWithTools = await getAllFlowItemsWithTools(); 88 | 89 | return { 90 | props: { 91 | flows, 92 | flowItemsWithTools 93 | }, 94 | revalidate: 60 95 | }; 96 | } -------------------------------------------------------------------------------- /pages/listings.js: -------------------------------------------------------------------------------- 1 | import Pricing from '../components/Pricing'; 2 | import LightHeroD from '../components/ui/Hero'; 3 | import ParagraphWithButton from '../components/ui/ParagraphWithButton'; 4 | import ListItem from '../components/ui/ListItem'; 5 | import TextList from '../components/ui/TextList'; 6 | import ListItemMirrored from '../components/ui/ListItemMirrored'; 7 | import Title from '../components/ui/Title'; 8 | import { getPublishedListings } from '../utils/supabase-client'; 9 | import SquareBlock from '../components/ui/SquareBlock'; 10 | import getRandomGradient from '../utils/getRandomGradient'; 11 | 12 | export default function UseCasePage({ listings }) { 13 | 14 | let groupArray = [] 15 | const uniqueGroups = [...new Set(listings.map(listing => listing.listing_type))]; 16 | const listingsByGroup = [...new Set(uniqueGroups.map(group => { 17 | return { 18 | category: group ? group : 'Default', 19 | itemArray: listings.filter(item => { 20 | if (item.listing_type == group){ 21 | return item 22 | } 23 | })} 24 | }))]; 25 | 26 | const listListings = uniqueGroups.map((group) => { 27 | const sortedItemArray = listingsByGroup.find(item => item.category == group) 28 | const itemElements = sortedItemArray ? sortedItemArray.itemArray.map(item => { 29 | 30 | return ( 31 | <SquareBlock key={item.id} blockId={item.id} blockBody={item.listing} 32 | blockDescription={item.description} 33 | ctaLink={item.url} 34 | ctaLinkTitle={'Open'} 35 | blockType={null} /> 36 | )}) : null 37 | 38 | let currentGroupTitle = group ? group : 'Default'; 39 | 40 | 41 | if (currentGroupTitle != 'Default' && currentGroupTitle){ 42 | groupArray.push(currentGroupTitle); 43 | 44 | return ( 45 | <ListItem key={currentGroupTitle.toString()} 46 | categoryName={currentGroupTitle.toString()} emoji={'🌐'} 47 | categoryDescription={''}> 48 | {itemElements} 49 | </ListItem>) 50 | } 51 | 52 | 53 | } 54 | ); 55 | 56 | return ( 57 | <> 58 | <div className="-mb-20 -mt-20"> 59 | <Title titleTitle={'Listings'} 60 | titleDescription={'Collections with useful information around the Web'} 61 | colorBg={getRandomGradient()} /> 62 | </div> 63 | <div className="flex space-between"> 64 | <aside className="h-screen sticky top-0 w-1/5 hidden md:flex"> 65 | <div className="pt-20 h-full"> 66 | <TextList items={groupArray} /> 67 | </div> 68 | </aside> 69 | <div className="flex-col w-full w-4/5 mt-14"> 70 | {listListings} 71 | </div> 72 | </div> 73 | {/*<ParagraphWithButton /> 74 | <Pricing products={products} />*/} 75 | </> 76 | ) 77 | } 78 | 79 | export async function getStaticProps() { 80 | const listings = await getPublishedListings(); 81 | 82 | return { 83 | props: { 84 | listings 85 | }, 86 | revalidate: 60 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /pages/myflows/index.tsx: -------------------------------------------------------------------------------- 1 | import router, { useRouter } from "next/router"; 2 | import { useEffect, useState } from "react"; 3 | import { useUser } from "../../utils/useUser"; 4 | import Link from "next/link"; 5 | import Image from "next/image"; 6 | import useToast from "../../components/hooks/useToast"; 7 | import Sidebar from '../../components/ui/Sidebar'; 8 | import ListItem from '../../components/ui/ListItem'; 9 | import TextList from '../../components/ui/TextList'; 10 | import ListItemMirrored from '../../components/ui/ListItemMirrored'; 11 | import Title from '../../components/ui/Title'; 12 | import { getAllFlowItems, getAllFlowItemsWithTools, getAllActions, getFlowsByAuthor } from '../../utils/supabase-client'; 13 | import SquareBlock from '../../components/ui/SquareBlock'; 14 | import PrettyBlock from '../../components/ui/PrettyBlock'; 15 | 16 | const limit = 10 17 | 18 | const MyFlowsPage = () => { 19 | 20 | const {user} = useUser() 21 | const {makeToast} = useToast() 22 | const router = useRouter() 23 | const [authorFlows, setAuthorFlows] = useState([]); 24 | const [flowItemsWithTools, setFlowItems] = useState([]); 25 | const [loading, setLoading] = useState(true) 26 | const [page, setPage] = useState(1) 27 | const map = new Map(); 28 | 29 | async function returnAuthorFlows(user_id){ 30 | if (user_id){ 31 | const authorFlowsResponse = await getFlowsByAuthor(user_id); 32 | setAuthorFlows(authorFlowsResponse); 33 | }} 34 | 35 | async function returnFlowItems(){ 36 | const flowItemsResponse = await getAllFlowItemsWithTools(); 37 | setFlowItems(flowItemsResponse); 38 | } 39 | 40 | useEffect(() => { 41 | if(user){ 42 | returnAuthorFlows(user.id); 43 | }}, [user]) 44 | 45 | useEffect(() => { 46 | returnFlowItems(); 47 | }, []) 48 | 49 | let sortedItemArray = authorFlows.slice(0,4); 50 | const listFlows = sortedItemArray.map(flow => { 51 | const currentFlowItems = flowItemsWithTools.filter(flowItem => flowItem.flow == flow.id); 52 | const allToolTitles = [...new Set(currentFlowItems.map(item => item.job_tool.tool.tool))]; 53 | const allToolImages = [...new Set(currentFlowItems.map(item => item.job_tool.tool.logo_url))]; 54 | return ( 55 | <PrettyBlock key={flow.id} smallImage={allToolImages[0] ? allToolImages[0] : null} 56 | blockLink={'/flow/' + flow.id} blockBody={flow.flow} 57 | flexibleHeight={true} fullWidth={true} 58 | blockDescription={`Using ${allToolTitles.toString().split(',').join(', ')}`} /> 59 | ) 60 | } 61 | ); 62 | 63 | 64 | 65 | return ( 66 | <> 67 | <div className="-mb-20 -mt-20"> 68 | <Title titleTitle={'My Guides'} 69 | titleDescription={'Welcome to your guides section'} /> 70 | </div> 71 | <div className="flex justify-center"> 72 | <div className="w-full flex flex-col items-end mb-24 mt-24 md:mt-16"> 73 | <Sidebar page="myflows" /> 74 | <div className="flex flex-col mx-auto px-4 w-full md:w-3/4 md:px-24 lg:px-48 mt-6 justify-start mb-24"> 75 | {(sortedItemArray.length > 0) ? (listFlows) : ( 76 | <div className="bg-gray-200 rounded w-full px-4 py-8 text-center"> 77 | <p>You don't have any private or public guides yet.</p> 78 | <p> Press "New guide" to start writing!</p></div>)} 79 | <div className="mr-4 px-4 mt-12 flex justify-end 80 | "> 81 | <Link href="/new-flow"> 82 | <a className="py-2 px-4 border border-black hover:bg-gray-50 rounded font-semibold"> 83 | + New Guide 84 | </a> 85 | </Link> 86 | </div> 87 | </div> 88 | </div> 89 | </div> 90 | </> 91 | ) 92 | } 93 | 94 | export default MyFlowsPage 95 | -------------------------------------------------------------------------------- /pages/new-flow.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import AutosizeInput from "react-input-autosize" 3 | import NewFlowContext from "../components/context/newFlow/newFlowContext" 4 | import Plus from "../components/icons/Plus" 5 | import ModalsContext from "../components/modals/modalsContext" 6 | import NewFlowInputInput from "../components/modals/newFlow/newFlowInputInput" 7 | import NewFlowOutputInput from "../components/modals/newFlow/newFlowOutputInput" 8 | import NewFlowStepInput from "../components/modals/newFlow/newFlowStepInput" 9 | import { useUser } from "../utils/useUser" 10 | 11 | const newFlow = () => { 12 | const { state, service } = useContext(ModalsContext) 13 | const {user} = useUser() 14 | const { newFlowState, newFlowService, dispatch } = useContext(NewFlowContext) 15 | return ( 16 | <div className={`w-full`}> 17 | <div 18 | onClick={(e) => { 19 | e.stopPropagation() 20 | }} 21 | onMouseUp={(e) => { 22 | e.stopPropagation() 23 | }} 24 | className={` 25 | overflow-y-hidden w-full md:w-2/3 mx-auto h-full relative bg-white rounded-lg opacity-100 visible scale-100 26 | transition-all duration-300 transform origin-center 27 | pb-10 overflow-x-hidden 28 | `}> 29 | <div className={`w-full mt-4 px-8 md:px-16 z-30 sticky left-0 h-20 top-0 flex px-4 pr-6 md:px-8 items-center `}> 30 | <div /> 31 | <AutosizeInput 32 | value={newFlowState.title} 33 | onChange={(e) => { 34 | newFlowService.dispatch({ type: "setTitle", payload: e.target.value }) 35 | }} 36 | inputStyle={{ backgroundColor: "#fff", outline: "none", fontWeight: "bold"}} 37 | className={`text-black text-center text-2xl font-bold`} 38 | /> 39 | </div> 40 | <div className="flex flex-col px-8 md:px-16"> 41 | <div className="mt-4"> 42 | <div> 43 | <div className={`text-lg font-semibold mb-4 w-full text-left ml-1`}>Inputs required</div> 44 | <div className={`grid grid-cols-1 gap-y-4`}> 45 | {newFlowState.inputs.map((input, index) => { 46 | return <NewFlowInputInput index={index} input={input} /> 47 | })} 48 | </div> 49 | <button 50 | onClick={() => { 51 | newFlowService.addInput() 52 | }} 53 | className={`bg-white border bg-gray-50 py-1 px-3 ml-3 my-4 focus:outline-none hover:bg-gray-200 rounded text-gray-500`}> 54 | + New input 55 | </button> 56 | </div> 57 | </div> 58 | <div className="mt-8"> 59 | <div className={`text-lg font-semibold mb-8 w-full text-left ml-1`}>Instruction steps</div> 60 | <div className={`grid grid-cols-1 gap-y-8`}> 61 | {newFlowState.steps.map((step, index) => { 62 | return ( 63 | <NewFlowStepInput step={step} index={index}/> 64 | ) 65 | })} 66 | </div> 67 | <button 68 | onClick={() => { 69 | newFlowService.addStep() 70 | }} 71 | className={`bg-white border bg-gray-50 py-1 px-3 ml-3 my-4 focus:outline-none hover:bg-gray-200 rounded text-gray-500`}> 72 | + New step 73 | </button> 74 | </div> 75 | <div className={`flex-col items-center flex mt-8`}> 76 | <div className={`text-lg font-semibold mb-4 w-full text-left ml-1`}>End result</div> 77 | <NewFlowOutputInput/> 78 | <div className="flex mt-12"> 79 | <input checked={newFlowState.isPrivate} onClick={() => {newFlowService.setChecked(!newFlowState.isPrivate)}} type="checkbox"/> 80 | <span className="ml-2">Make private?</span> 81 | </div> 82 | <button onClick={async () => { 83 | if (user.id) { 84 | await newFlowService.saveFlow(user.id) 85 | //service.closeModal("newFlow") 86 | } 87 | }} className={`inline-flex items-center bg-white border border-black py-1 px-3 mt-4 focus:outline-none hover:bg-gray-200 rounded text-base`}> 88 | {newFlowState.loading ? "loading..." : "Publish guide"} 89 | </button> 90 | </div> 91 | </div> 92 | </div> 93 | </div> 94 | ) 95 | } 96 | 97 | export default newFlow -------------------------------------------------------------------------------- /pages/output/[output_id].js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import Pricing from '../../components/Pricing'; 3 | import LightHeroD from '../../components/ui/Hero'; 4 | import ParagraphWithButton from '../../components/ui/ParagraphWithButton'; 5 | import ListItem from '../../components/ui/ListItem'; 6 | import TextList from '../../components/ui/TextList'; 7 | import ListItemMirrored from '../../components/ui/ListItemMirrored'; 8 | import Title from '../../components/ui/Title'; 9 | import { getFlowItemsByFlowId, 10 | getAllOutputs, getOutputById, getAllJobGroups, getFlowOutputsByOutputId, getFlowById, getAllFlows } from '../../utils/supabase-client'; 11 | //import MyDisclosure from '@/components/dynamic/disclosure'; 12 | //import SquareBlock from '../../components/ui/SquareBlock'; 13 | import PrettyBlock from '../../components/ui/PrettyBlock'; 14 | import getRandomGradient from '../../utils/getRandomGradient'; 15 | 16 | export default function Output({ products, jobGroups, 17 | output, flowsOutputs, allFlows }) { 18 | 19 | const router = useRouter() 20 | if (router.isFallback) { 21 | return (<div className="py-36"> 22 | <h1 className="text-2xl text-center">Nothing here... <a href="/" className="text-blue-600 hover:underline">Go home?</a></h1> 23 | </div>) 24 | } else { 25 | const { output_id } = router.query 26 | 27 | let groupArray = []; 28 | let flowIds = []; 29 | let currentOutput = output ? output[0] : null; 30 | 31 | const itemElements = flowsOutputs.map(item => { 32 | 33 | let currentFlow = []; 34 | if (item != null){ 35 | currentFlow = allFlows.find(flow => flow.id == item.flow) 36 | } 37 | 38 | return ( 39 | <PrettyBlock key={currentFlow.id} blockBody={currentFlow.flow} 40 | blockLink={'/flow/' + currentFlow.id} /> 41 | ) 42 | } 43 | ) 44 | 45 | 46 | return ( 47 | <> 48 | <div className="-mb-20 -mt-20"> 49 | <Title titleTitle={currentOutput.output} titleDescription={currentOutput.description} 50 | colorBg={getRandomGradient()} /> 51 | </div> 52 | <div className="flex space-between"> 53 | {/*<aside className="h-screen sticky top-0 w-1/5 hidden md:block"> 54 | <div className="pt-20 h-full"> 55 | <TextList items={groupArray} /> 56 | </div> 57 | </aside>*/} 58 | <div className="flex-col w-full mx-auto md:w-4/5 mt-14 py-14"> 59 | <h2 className="lg:w-4/5 text-center mx-auto px-6 sm:text-2xl text-xl text-gray-900"> 60 | Get {currentOutput.output.toLowerCase()} by following these flows</h2> 61 | <div className="flex items-center justify-center w-full mx-auto lg:w-4/5 mt-4"> 62 | {itemElements} 63 | </div> 64 | </div> 65 | </div> 66 | </> 67 | )} 68 | } 69 | 70 | export async function getStaticPaths() { 71 | const outputs = await getAllOutputs(); 72 | let outputIds = []; 73 | outputs.map(output => outputIds.push({params: {output_id: output.id.toString()}})) 74 | 75 | return { 76 | paths: outputIds, 77 | fallback: true, 78 | } 79 | } 80 | 81 | export async function getStaticProps(context) { 82 | 83 | const jobGroups = await getAllJobGroups(); 84 | const allFlows = await getAllFlows(); 85 | const flowsOutputs = await getFlowOutputsByOutputId(context.params.output_id); 86 | // const flowItems = await getFlowItemsByFlowId(flowsOutputs[0].flow); 87 | const output = await getOutputById(context.params.output_id); 88 | 89 | return { 90 | props: { 91 | 92 | jobGroups, 93 | flowsOutputs, 94 | output, 95 | allFlows 96 | }, 97 | revalidate: 60 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /pages/recovery.tsx: -------------------------------------------------------------------------------- 1 | import router, { useRouter } from "next/router"; 2 | import { FormEvent, useState } from "react"; 3 | import { userAdapter } from "../adapters/user/adapter"; 4 | import Logo from "../components/icons/Logo"; 5 | import Button from "../components/ui/Button"; 6 | import Input from "../components/ui/Input"; 7 | import { useUser } from '../utils/useUser'; 8 | 9 | const RecoveryPage = () => { 10 | const [ password, setPassword ] = useState(''); 11 | const { user, session } = useUser(); 12 | const [ message, setMessage ] = useState({ type: '', content: '' }); 13 | const { push } = useRouter() 14 | 15 | const handlePasschange = async (e: FormEvent<HTMLFormElement>) => { 16 | e.preventDefault() 17 | const response = await userAdapter.recoverPassword(session.access_token, password) 18 | if (response.error) { 19 | setMessage({type: "error", content: response.error.message}) 20 | return 21 | } 22 | if (response.data) { 23 | setMessage({"type": "message", content: "Done! Redirecting!"}) 24 | router.push("/") 25 | } 26 | } 27 | 28 | if (user) { 29 | return ( 30 | <div className="w-80 flex flex-col justify-between p-3 max-w-lg m-auto mb-24"> 31 | <h3 className="text-3xl text-center my-6">Hi there!</h3> 32 | <div className="flex justify-center pb-12"> 33 | <Logo width="64px" height="64px" /> 34 | </div> 35 | <div className="flex flex-col space-y-4"> 36 | {message.content && ( 37 | <div 38 | className={`${ 39 | message.type === 'error' ? 'text-pink' : 'text-green' 40 | } border ${ 41 | message.type === 'error' ? 'border-pink' : 'border-green' 42 | } p-3`} 43 | > 44 | {message.content} 45 | </div> 46 | )} 47 | <form onSubmit={handlePasschange} className="flex flex-col space-y-4"> 48 | <Input 49 | type="password" 50 | placeholder="Password" 51 | value={password} 52 | onChange={setPassword} 53 | required 54 | /> 55 | <Button 56 | className="mt-1" 57 | variant="slim" 58 | type="submit" 59 | disabled={!password.length} 60 | > 61 | Change Password 62 | </Button> 63 | </form> 64 | </div> 65 | </div> 66 | ) 67 | } 68 | return ( 69 | <> 70 | </> 71 | ) 72 | 73 | } 74 | 75 | export default RecoveryPage; -------------------------------------------------------------------------------- /pages/search.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import useDebounce from "../components/hooks/useDebounce" 3 | import { SupabaseServiceClass } from "../utils/supabase-client" 4 | import Link from "next/link" 5 | import SquareBlock from '../components/ui/SquareBlock'; 6 | 7 | class SearchService extends SupabaseServiceClass { 8 | async searchByString(string:string) { 9 | const {data, error} = await this.supabase 10 | .rpc("find_entries", { 11 | search_input: string 12 | }) 13 | return data 14 | } 15 | } 16 | const searchService = new SearchService() 17 | 18 | interface SearchResult { 19 | type: string, 20 | id: number, 21 | name: string 22 | } 23 | 24 | const Search = () => { 25 | const [searchString, setSearchString] = useState("") 26 | const [searchResults, setSearchResults] = useState<SearchResult[]>([]) 27 | const debouncedSearchString = useDebounce(searchString, 1000) 28 | 29 | 30 | useEffect(() => { 31 | if (debouncedSearchString) { 32 | searchService 33 | .searchByString(debouncedSearchString) 34 | .then((data) => { 35 | setSearchResults(data) 36 | }) 37 | } 38 | }, [debouncedSearchString]) 39 | 40 | return ( 41 | <div className={`w-full flex flex-col`}> 42 | <input 43 | value={searchString} 44 | onChange={(E) => { 45 | setSearchString(E.target.value) 46 | }} 47 | className={`mb-5 w-full h-12 px-5 text-lg`} 48 | type="text" 49 | placeholder="Type to search for posts and tools..." 50 | name="" 51 | id="" /> 52 | {searchResults.map((res) => { 53 | return <Link 54 | href={`/${res.type}/${res.id}`} 55 | passHref={true} 56 | 57 | > 58 | <a className={`w-full border border-gray-200 hover:bg-gray-200 mb-2`} 59 | href=" 60 | "> 61 | <SquareBlock key={`${res.type}/${res.id}`} 62 | blockBody={res.name} 63 | blockDescription={res.type} 64 | /> 65 | </a> 66 | </Link> 67 | })} 68 | </div> 69 | ) 70 | } 71 | 72 | export default Search -------------------------------------------------------------------------------- /pages/signup.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useRouter } from 'next/router'; 3 | import { useEffect, useState } from 'react'; 4 | import { signInWithGoogle } from '../utils/supabase-client'; 5 | 6 | import GoogleButton from 'react-google-button' 7 | import Button from '../components/ui/Button'; 8 | import Input from '../components/ui/Input'; 9 | import Logo from '../components/icons/Logo'; 10 | import { updateUserName } from '../utils/supabase-client'; 11 | import { useUser } from '../utils/useUser'; 12 | 13 | const SignUp = () => { 14 | const [user, setUser] = useState(null); 15 | const [email, setEmail] = useState(''); 16 | const [password, setPassword] = useState(''); 17 | const [name, setName] = useState(''); 18 | const [loading, setLoading] = useState(false); 19 | const [message, setMessage] = useState({ type: '', content: '' }); 20 | const router = useRouter(); 21 | const { signUp } = useUser(); 22 | 23 | const handleSignup = async (e) => { 24 | e.preventDefault(); 25 | 26 | setLoading(true); 27 | setMessage({}); 28 | const { error, user } = await signUp({ email, password }); 29 | if (error) { 30 | setMessage({ type: 'error', content: error.message }); 31 | } else { 32 | if (user) { 33 | await updateUserName(user, name); 34 | setUser(user); 35 | } else { 36 | setMessage({ 37 | type: 'note', 38 | content: 'Check your email for the confirmation link.' 39 | }); 40 | } 41 | } 42 | setLoading(false); 43 | }; 44 | 45 | useEffect(() => { 46 | if (user) { 47 | router.replace('/account'); 48 | } 49 | }, [user]); 50 | 51 | return ( 52 | <form 53 | onSubmit={handleSignup} 54 | className="w-80 flex flex-col justify-between p-3 max-w-lg m-auto mb-24" 55 | > 56 | <div className="flex justify-center pb-12 "> 57 | <Logo width="64px" height="64px" /> 58 | </div> 59 | <div className="flex flex-col space-y-4"> 60 | <div className="mt-2 mb-8"> 61 | Unfortunately, Gardens has been shut down. Reach out to us at <a href="mailto:hello@joingardens.com">hello@joingardens.com</a> if you have a question or a proposal. 62 | </div> 63 | </div> 64 | </form> 65 | ); 66 | }; 67 | 68 | export default SignUp; 69 | -------------------------------------------------------------------------------- /pages/tasks.js: -------------------------------------------------------------------------------- 1 | import Pricing from '../components/Pricing'; 2 | import LightHeroD from '../components/ui/Hero'; 3 | import ParagraphWithButton from '../components/ui/ParagraphWithButton'; 4 | import ListItem from '../components/ui/ListItem'; 5 | import TextList from '../components/ui/TextList'; 6 | import ListItemMirrored from '../components/ui/ListItemMirrored'; 7 | import Title from '../components/ui/Title'; 8 | import { getAllJobs, getAllJobGroups, getAllJobTools } from '../utils/supabase-client'; 9 | import SquareBlock from '../components/ui/SquareBlock'; 10 | import getRandomGradient from '../utils/getRandomGradient'; 11 | 12 | export default function JobsPage({ products, jobs, jobGroups, jobTools }) { 13 | 14 | let groupArray = [] 15 | const uniqueGroups = [...new Set(jobs.map(job => job.job_group))]; 16 | const jobsByCategory = [...new Set(uniqueGroups.map(group => { 17 | return { 18 | category: group ? jobGroups.find(item => item.id == group) : 'Default', 19 | itemArray: jobs.filter(item => { 20 | if (item.job_group == group){ 21 | return item 22 | } 23 | })} 24 | }))]; 25 | 26 | const listJobs = uniqueGroups.map((group) => { 27 | const filteredArray = jobsByCategory.filter(item => { 28 | if (item.category.id == group){ 29 | return item 30 | } 31 | }) 32 | const itemArray = filteredArray[0].itemArray; 33 | const sortedItemArray = itemArray 34 | const itemElements = sortedItemArray.map(item => { 35 | let currentJobTool = jobTools.find(jobTool => jobTool.job == item.id) 36 | return (<SquareBlock key={item.id} blockBody={item.job} 37 | ctaLink={(currentJobTool) ? ('/usecases#' + currentJobTool.id) : null} 38 | ctaLinkTitle={'Learn more'} 39 | blockType={''} />) 40 | } 41 | ) 42 | let currentGroup = jobGroups.find(item => item.id == group) 43 | let currentGroupTitle = currentGroup ? currentGroup.job_group : 'Default' 44 | groupArray.push(currentGroupTitle) 45 | 46 | return ( 47 | <ListItem key={currentGroupTitle.toString()} categoryName={currentGroupTitle.toString()} emoji={'📂'} categoryDescription={''}> 48 | {itemElements} 49 | </ListItem>) 50 | } 51 | ); 52 | 53 | return ( 54 | <> 55 | <div className="-mb-20 -mt-20"> 56 | <Title titleTitle={'Tasks'} 57 | titleDescription={'All tasks, sorted by task group'} 58 | colorBg={getRandomGradient()} /> 59 | </div> 60 | <div className="flex space-between"> 61 | <aside className="h-screen sticky top-0 w-1/5 hidden md:block"> 62 | <div className="pt-20 h-full"> 63 | <TextList items={groupArray} /> 64 | </div> 65 | </aside> 66 | <div className="flex-col w-full md:w-4/5 mt-14"> 67 | {listJobs} 68 | </div> 69 | </div> 70 | {/*<ParagraphWithButton /> 71 | <Pricing products={products} />*/} 72 | </> 73 | ) 74 | } 75 | 76 | export async function getStaticProps() { 77 | 78 | const jobs = await getAllJobs(); 79 | const jobGroups = await getAllJobGroups(); 80 | const jobTools = await getAllJobTools(); 81 | 82 | return { 83 | props: { 84 | 85 | jobs, 86 | jobGroups, 87 | jobTools 88 | }, 89 | revalidate: 60 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /pages/tools.js: -------------------------------------------------------------------------------- 1 | import Pricing from '../components/Pricing'; 2 | import LightHeroD from '../components/ui/Hero'; 3 | import Link from 'next/link'; 4 | import ParagraphWithButton from '../components/ui/ParagraphWithButton'; 5 | import ListItem from '../components/ui/ListItem'; 6 | import TextList from '../components/ui/TextList'; 7 | import ListItemMirrored from '../components/ui/ListItemMirrored'; 8 | import Title from '../components/ui/Title'; 9 | import { getAllTools, getAllJobTools } from '../utils/supabase-client'; 10 | import urlify from '../utils/helpers.js'; 11 | import SquareBlock from '../components/ui/SquareBlock'; 12 | import PrettyBlock from '../components/ui/PrettyBlock'; 13 | import getRandomGradient from '../utils/getRandomGradient'; 14 | import SectionsAndCategories from '../components/ui/SectionsAndCategories'; 15 | 16 | export default function ToolsPage({ products, tools, jobTools }) { 17 | 18 | let toolsWithJobs = []; 19 | jobTools.map(jobTool => toolsWithJobs.push(jobTool.tool)) 20 | const uniqueCategories = [...new Set(tools.map(tool => tool.category))]; 21 | const uniqueSections = [...new Set(tools.map(tool => tool.section))]; 22 | const sortedSections = uniqueSections.sort((a, b) => {if (a && b){ 23 | if(a == "General"){ 24 | return -2 25 | } else { 26 | return a.localeCompare(b) 27 | } 28 | }} ); 29 | let categoriesWithSections = sortedSections.map(section => ({section: section, categories: []})); 30 | tools.map(tool => { 31 | let currentSection = categoriesWithSections.findIndex(categoryWithSection => categoryWithSection.section == tool.section); 32 | (!categoriesWithSections[currentSection].categories.includes(tool.category)) ? categoriesWithSections[currentSection].categories.push(tool.category) : null 33 | }); 34 | const toolsByCategory = [...new Set(uniqueCategories.map(category => { 35 | return { 36 | category: category, 37 | itemArray: tools.filter(item => { 38 | if (item.category == category){ 39 | return item 40 | } 41 | })} 42 | }))]; 43 | 44 | const featuredElements = uniqueCategories.map(category => { 45 | if (category && (category.length != 0)){ 46 | 47 | return ( 48 | <PrettyBlock key={category ? urlify(category) : ''} 49 | blockBody={category} blockLink={category ? ('#' + urlify(category)) : ''} linkTitle={'View'} 50 | blockType={'Open'} />) 51 | } 52 | }); 53 | 54 | const listTools = uniqueCategories.map((category) => { 55 | const filteredArray = toolsByCategory.filter(item => { 56 | if (item.category == category){ 57 | return item 58 | } 59 | }); 60 | const itemArray = filteredArray[0].itemArray; 61 | const sortedItemArray = itemArray.sort((a, b) => a.model - b.model) 62 | 63 | const itemElements = sortedItemArray.map(item => 64 | { 65 | let isOneClick = (item.one_click) ? true : false; 66 | return (<SquareBlock key={item.id} blockBody={item.tool} 67 | smallImage={item.logo_url} smallImageAlt={item.tool + ' logo'} 68 | blockLink={isOneClick ? null : item.link} 69 | blockLinkTitle={isOneClick ? null : 'Press to open website'} 70 | ctaLink={isOneClick ? ('/tool/' + item.id) : null} 71 | ctaLinkTitle={isOneClick ? ('Press to self-host ' + item.tool + '!') : null} 72 | blockDescription={item.description} 73 | blockColor={(isOneClick && item.model == 1) ? 'Green' : ((isOneClick && item.model == 2) ? 'Blue' : null)} 74 | blockType={(item.model == 1) ? 'Open' : ((item.model == 2) ? 'Fair' : ((item.model == 3) ? 'Exportable' : ((item.model == 4) ? 'Closed' : null)))} /> 75 | ) 76 | }); 77 | 78 | if (category && (category.length != 0)){ 79 | return ( 80 | <ListItem key={category.toString()} noUrl={true} 81 | categoryName={category.toString()} categoryDescription={''} 82 | addLink={'https://tally.so/r/w8Zo5m' + '?category=' + category.toString()}> 83 | {itemElements} 84 | </ListItem>) 85 | } 86 | 87 | 88 | } 89 | ); 90 | 91 | 92 | return ( 93 | <> 94 | <div className="-mb-20 -mt-20"> 95 | <Title titleTitle={'Tools'} 96 | titleDescription={'Open-source and fair-code tools are shown at the top. Pick a category — or keep scrolling to discover all tools.'} 97 | /> 98 | </div> 99 | <div className="flex space-between"> 100 | <aside className="h-screen sticky top-0 w-1/5 hidden md:block"> 101 | <div className="pt-20 h-full"> 102 | <TextList items={uniqueCategories} /> 103 | </div> 104 | </aside> 105 | <div className="flex-col w-full w-4/5 mt-4"> 106 | <SectionsAndCategories sections={categoriesWithSections} /> 107 | {listTools} 108 | </div> 109 | </div> 110 | </> 111 | ) 112 | } 113 | 114 | export async function getStaticProps() { 115 | 116 | const tools = await getAllTools(); 117 | const jobTools = await getAllJobTools(); 118 | 119 | return { 120 | props: { 121 | 122 | tools, 123 | jobTools 124 | }, 125 | revalidate: 60 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /pages/usecases.js: -------------------------------------------------------------------------------- 1 | import Pricing from '../components/Pricing'; 2 | import LightHeroD from '../components/ui/Hero'; 3 | import ParagraphWithButton from '../components/ui/ParagraphWithButton'; 4 | import ListItem from '../components/ui/ListItem'; 5 | import TextList from '../components/ui/TextList'; 6 | import ListItemMirrored from '../components/ui/ListItemMirrored'; 7 | import Title from '../components/ui/Title'; 8 | import { getAllJobs, getAllJobGroups, getAllJobTools, getAllTools } from '../utils/supabase-client'; 9 | import SquareBlock from '../components/ui/SquareBlock'; 10 | import getRandomGradient from '../utils/getRandomGradient'; 11 | 12 | export default function UseCasePage({ products, jobs, jobGroups, tools, jobTools }) { 13 | 14 | let groupArray = [] 15 | const uniqueGroups = [...new Set(jobTools.map(jobTool => jobTool.job))]; 16 | const toolsByJob = [...new Set(uniqueGroups.map(group => { 17 | return { 18 | category: group ? jobs.find(item => item.id == group) : 'Default', 19 | itemArray: jobTools.filter(item => { 20 | if (item.job == group){ 21 | return item 22 | } 23 | })} 24 | }))]; 25 | 26 | const listJobs = uniqueGroups.map((group) => { 27 | const filteredArray = toolsByJob.filter(item => { 28 | if (item.category.id == group){ 29 | return item 30 | } 31 | }) 32 | const itemArray = filteredArray[0].itemArray; 33 | const sortedItemArray = itemArray 34 | const itemElements = sortedItemArray.map(item => { 35 | let currentTool = tools.find(tool => tool.id == item.tool); 36 | let currentJob = jobs.find(job => job.id == item.job); 37 | return ( 38 | <SquareBlock key={item.id} blockId={item.id} blockBody={currentJob.job} 39 | blockDescription={'Using '} 40 | blockDescriptionLinkTitle={currentTool ? currentTool.tool : 'No tool'} 41 | ctaLink={currentTool ? ('/tool/' + item.tool) : null} 42 | ctaLinkTitle={'Press to get this done'} 43 | blockType={currentTool ? ((currentTool.model == 1) ? 'Open' : (currentTool.model == 2) ? 'Fair' : (currentTool.model == 4) ? 'Closed' : (currentTool.model == 3) ? 'Exportable' : null) : null} />) 44 | 45 | } 46 | ) 47 | let currentGroup = jobGroups.find(item => item.id == group) 48 | let currentGroupTitle = currentGroup ? currentGroup.job_group : 'Default'; 49 | (currentGroupTitle != 'Default') ? groupArray.push(currentGroupTitle) : null 50 | 51 | if (currentGroupTitle != 'Default'){ 52 | return ( 53 | <ListItem key={currentGroupTitle.toString()} 54 | categoryName={currentGroupTitle.toString()} emoji={'📘'} 55 | categoryDescription={''}> 56 | {itemElements} 57 | </ListItem>) 58 | } 59 | 60 | 61 | } 62 | ); 63 | 64 | return ( 65 | <> 66 | <div className="-mb-20 -mt-20"> 67 | <Title titleTitle={'Use cases'} 68 | titleDescription={'Execute common tasks using your preferred tools'} 69 | colorBg={getRandomGradient()} /> 70 | </div> 71 | <div className="flex space-between"> 72 | <aside className="h-screen sticky top-0 w-1/5 hidden md:flex"> 73 | <div className="pt-20 h-full"> 74 | <TextList items={groupArray} /> 75 | </div> 76 | </aside> 77 | <div className="flex-col w-full w-4/5 mt-14"> 78 | {listJobs} 79 | </div> 80 | </div> 81 | {/*<ParagraphWithButton /> 82 | <Pricing products={products} />*/} 83 | </> 84 | ) 85 | } 86 | 87 | export async function getStaticProps() { 88 | 89 | const jobs = await getAllJobs(); 90 | const tools = await getAllTools(); 91 | const jobGroups = await getAllJobGroups(); 92 | const jobTools = await getAllJobTools(); 93 | 94 | return { 95 | props: { 96 | 97 | jobs, 98 | jobGroups, 99 | jobTools, 100 | tools 101 | }, 102 | revalidate: 60 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'tailwindcss', 4 | 'postcss-nesting', 5 | 'postcss-flexbugs-fixes', 6 | [ 7 | 'postcss-preset-env', 8 | { 9 | autoprefixer: { 10 | flexbox: 'no-2009' 11 | }, 12 | stage: 3, 13 | features: { 14 | 'custom-properties': false 15 | } 16 | } 17 | ] 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /public/OW_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joingardens/gardens/99eb6816cceeb30ae9bcedde4f7abbb14db7f245/public/OW_favicon.png -------------------------------------------------------------------------------- /public/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joingardens/gardens/99eb6816cceeb30ae9bcedde4f7abbb14db7f245/public/demo.png -------------------------------------------------------------------------------- /public/github.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 2880 1024"><path fill="white" fill-rule="evenodd" d="M1185.92 769.928h-1.28c.576 0 .96.576 1.536.704.064 0 .32-.064.384-.064l-.64-.64zm.256.704c-5.952.064-20.928 3.136-36.736 3.136-49.92 0-67.2-23.042-67.2-53.122v-200.32H1184c5.76 0 10.24-5.12 10.24-12.16v-108.8c0-5.76-5.12-10.88-10.24-10.88h-101.76v-135.04c0-5.12-3.2-8.32-8.96-8.32H935.04c-5.76 0-8.96 3.2-8.96 8.32v138.88s-69.76 17.28-74.24 17.92c-5.12 1.28-8.32 5.76-8.32 10.88v87.04c0 7.04 5.12 12.16 10.88 12.16h71.04v209.92c0 156.162 108.8 172.162 183.04 172.162 33.92 0 74.88-10.88 81.28-14.08 3.84-1.28 5.76-5.76 5.76-10.24v-96c0-6.336-4.352-10.56-9.344-11.456zM2702.72 629.766c0-115.84-46.72-131.2-96-126.08-38.4 2.56-69.12 21.76-69.12 21.76v225.28s31.36 21.762 78.08 23.042c65.92 1.92 87.04-21.762 87.04-144.002zm155.52-10.24c0 219.522-71.04 282.242-195.2 282.242-104.96 0-161.28-53.12-161.28-53.12s-2.56 29.44-5.76 33.28c-1.92 3.84-5.12 5.12-8.96 5.12h-94.72c-6.4 0-12.16-5.12-12.16-10.88l1.28-711.042c0-5.76 5.12-10.88 10.88-10.88h136.32c5.76 0 10.88 5.12 10.88 10.88v241.28s52.48-33.92 129.28-33.92l-.64-1.28c76.8 0 190.08 28.8 190.08 248.32zm-558.08-231.04H2165.76c-7.04 0-10.88 5.12-10.88 12.16v348.16s-35.2 24.962-83.2 24.962-62.08-21.762-62.08-69.762v-304c0-5.76-5.12-10.88-10.88-10.88h-136.96c-5.76 0-10.88 5.12-10.88 10.88v327.04c0 140.802 78.72 176.002 186.88 176.002 88.96 0 161.28-49.28 161.28-49.28s3.2 24.96 5.12 28.8c1.28 3.2 5.76 5.76 10.24 5.76h85.76c7.04 0 10.88-5.12 10.88-10.88l1.28-478.082c0-5.76-5.12-10.88-12.16-10.88zm-1516.8-.64H647.04c-5.76 0-10.88 5.76-10.88 12.8v469.762c0 12.8 8.32 17.28 19.2 17.28h122.88c12.8 0 16-5.76 16-17.28V398.726c0-5.76-5.12-10.88-10.88-10.88zm-67.2-216.32c-49.28 0-88.32 39.04-88.32 88.32 0 49.28 39.04 88.32 88.32 88.32 48 0 87.04-39.04 87.04-88.32 0-49.28-39.04-88.32-87.04-88.32zm1055.36-16h-135.04c-5.76 0-10.88 5.12-10.88 10.88v261.76h-211.84v-261.76c0-5.76-5.12-10.88-10.88-10.88h-136.32c-5.76 0-10.88 5.12-10.88 10.88v711.042c0 5.76 5.76 10.88 10.88 10.88h136.32c5.76 0 10.88-5.12 10.88-10.88V573.446h211.84l-1.28 304.002c0 5.76 5.12 10.88 10.88 10.88h136.32c5.76 0 10.88-5.12 10.88-10.88V166.406c0-5.76-5.12-10.88-10.88-10.88zM563.84 470.406v367.362c0 2.56-.64 7.04-3.84 8.32 0 0-80 56.96-211.84 56.96C188.8 903.048 0 853.128 0 524.166c0-328.96 165.12-396.8 326.4-396.16 139.52 0 195.84 31.36 204.8 37.12 2.56 3.2 3.84 5.76 3.84 8.96l-26.88 113.92c0 5.76-5.76 12.8-12.8 10.88-23.04-7.04-57.6-21.12-138.88-21.12-94.08 0-195.2 26.88-195.2 238.72s96 236.8 165.12 236.8c58.88 0 80-7.04 80-7.04v-147.2h-94.08c-7.04 0-12.16-5.12-12.16-10.88v-117.76c0-5.76 5.12-10.88 12.16-10.88h239.36c7.04 0 12.16 5.12 12.16 10.88z" clip-rule="evenodd"/></svg> -------------------------------------------------------------------------------- /public/nextjs.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="207px" height="124px" viewBox="0 0 207 124" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <!-- Generator: Sketch 51.2 (57519) - http://www.bohemiancoding.com/sketch --> 4 | <title>next-white-vector 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joingardens/gardens/99eb6816cceeb30ae9bcedde4f7abbb14db7f245/public/og.png -------------------------------------------------------------------------------- /public/stripe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 12 | 15 | 16 | 17 | 18 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/vercel-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joingardens/gardens/99eb6816cceeb30ae9bcedde4f7abbb14db7f245/public/vercel-deploy.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /services/validationService.ts: -------------------------------------------------------------------------------- 1 | class ValidationService { 2 | validateSubdomainName(string: string) { 3 | if (!string) { 4 | return false 5 | } 6 | const validationRegex = new RegExp(/[a-z0-9](?:[A-Za-z0-9\-]{0,61}[a-z0-9])/) 7 | const matchResult = string.match(validationRegex) 8 | if (!matchResult) { 9 | return false 10 | } 11 | return matchResult.input === matchResult[0] 12 | } 13 | 14 | validateOrganisationName(string: string) { 15 | if (string.length > 3 && string.length < 100) { 16 | return true 17 | } 18 | return false 19 | } 20 | 21 | validateEmail(string: string) { 22 | if (!string) { 23 | return false 24 | } 25 | const validationRegex = new RegExp(/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/) 26 | const matchResult = string.match(validationRegex) 27 | if (!matchResult) { 28 | return false 29 | } 30 | return matchResult.input === matchResult[0] 31 | } 32 | 33 | validateDomainName(string: string) { 34 | if (!string) { 35 | return false 36 | } 37 | const validationRegex = new RegExp(/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/) 38 | const matchResult = string.match(validationRegex) 39 | if (!matchResult) { 40 | return false 41 | } 42 | return matchResult.input === matchResult[0] 43 | } 44 | } 45 | 46 | export const validationService = new ValidationService() 47 | 48 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', './utils/getRandomGradient.js'], 3 | darkMode: 'class', 4 | theme: { 5 | extend: { 6 | maxWidth: { 7 | '8xl': '1920px' 8 | }, 9 | placeholderColor: { 10 | 'primary': '#000', 11 | 'secondary': '#000', 12 | }, 13 | colors: { 14 | 'sapphire': '#05668D', 15 | 'seaweed': '#028090', 16 | 'persiangreen': '#00a896', 17 | 'meadow': '#02c39a', 18 | 'palebud': '#f0f3bd', 19 | primary: 'var(--primary)', 20 | 'primary-2': 'var(--primary-2)', 21 | secondary: 'var(--secondary)', 22 | 'secondary-2': 'var(--secondary-2)', 23 | hover: 'var(--hover)', 24 | 'hover-1': 'var(--hover-1)', 25 | 'hover-2': 'var(--hover-2)', 26 | 'accents-0': 'var(--accents-0)', 27 | 'accents-1': 'var(--accents-1)', 28 | 'accents-2': 'var(--accents-2)', 29 | 'accents-3': 'var(--accents-3)', 30 | 'accents-4': 'var(--accents-4)', 31 | 'accents-5': 'var(--accents-5)', 32 | 'accents-6': 'var(--accents-6)', 33 | 'accents-7': 'var(--accents-7)', 34 | 'accents-8': 'var(--accents-8)', 35 | 'accents-9': 'var(--accents-9)', 36 | 'violet-light': 'var(--violet-light)', 37 | }, 38 | textColor: { 39 | base: 'var(--text-base)', 40 | primary: 'var(--text-primary)', 41 | secondary: 'var(--text-secondary)' 42 | }, 43 | boxShadow: { 44 | 'outline-2': '0 0 0 2px var(--accents-2)', 45 | magical: 46 | 'rgba(0, 0, 0, 0.02) 0px 30px 30px, rgba(0, 0, 0, 0.03) 0px 0px 8px, rgba(0, 0, 0, 0.05) 0px 1px 0px' 47 | }, 48 | lineHeight: { 49 | 'extra-loose': '2.2' 50 | }, 51 | letterSpacing: { 52 | widest: '0.3em' 53 | } 54 | } 55 | }, 56 | plugins: [ 57 | require('@tailwindcss/typography') 58 | ] 59 | }; 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "downlevelIteration": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | , "pages/flows.js", "components/ui/Button/Button.tsx" ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /utils/autosize.ts: -------------------------------------------------------------------------------- 1 | export default function autosize(target: HTMLTextAreaElement): void { 2 | target.style.height = 'initial'; 3 | target.style.height = +target.scrollHeight + 'px'; 4 | } -------------------------------------------------------------------------------- /utils/getFlagUrl.ts: -------------------------------------------------------------------------------- 1 | const getFlagUrl = (TwoDigitCountry: string) => { 2 | return `https://catamphetamine.gitlab.io/country-flag-icons/3x2/${TwoDigitCountry}.svg` 3 | } 4 | 5 | export const flagUrlMap = { 6 | "san francisco": getFlagUrl("US"), 7 | "new york": getFlagUrl('US'), 8 | "singapore": getFlagUrl("SG"), 9 | "london": getFlagUrl("GB"), 10 | "amsterdam": getFlagUrl("NL"), 11 | "frankfurt": getFlagUrl("DE"), 12 | "toronto": getFlagUrl('CA'), 13 | 'bangalore': getFlagUrl('IN') 14 | } -------------------------------------------------------------------------------- /utils/getRandomGradient.js: -------------------------------------------------------------------------------- 1 | export default function getRandomGradient(){ 2 | 3 | // This is repetitive because https://tailwindcss.com/docs/optimizing-for-production#writing-purgeable-html 4 | 5 | const colors = [ 6 | 'bg-seaweed bg-opacity-20 hover:bg-opacity-30', 7 | 'bg-persiangreen bg-opacity-20 hover:bg-opacity-30', 8 | 'bg-meadow bg-opacity-20 hover:bg-opacity-30' 9 | ] 10 | /* Old version 11 | 'bg-gradient-to-b from-green-100 to-green-300', 12 | 'bg-gradient-to-b from-yellow-100 to-yellow-300', 13 | 'bg-gradient-to-b from-pink-100 to-pink-300', 14 | 'bg-gradient-to-b from-purple-100 to-purple-300', 15 | 'bg-gradient-to-b from-indigo-100 to-indigo-300', 16 | 'bg-gradient-to-b from-red-100 to-red-300', 17 | 'bg-gradient-to-b from-blue-100 to-blue-300' 18 | */ 19 | 20 | const randomColor = colors[Math.floor(Math.random() * colors.length)] 21 | 22 | return randomColor 23 | } -------------------------------------------------------------------------------- /utils/helpers.js: -------------------------------------------------------------------------------- 1 | export const getURL = () => { 2 | const url = 3 | process?.env?.URL && process.env.URL !== '' 4 | ? process.env.URL 5 | : process?.env?.VERCEL_URL && process.env.VERCEL_URL !== '' 6 | ? process.env.VERCEL_URL 7 | : 'http://localhost:3000'; 8 | return url.includes('http') ? url : `https://${url}`; 9 | }; 10 | 11 | export default function urlify(str) { 12 | str = str.replace(/^\s+|\s+$/g, ''); // trim 13 | str = str.toLowerCase(); 14 | 15 | // remove accents, swap ñ for n, etc 16 | var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;"; 17 | var to = "aaaaeeeeiiiioooouuuunc------"; 18 | for (var i=0, l=from.length ; i { 30 | const res = await fetch(url, { 31 | method: 'POST', 32 | headers: new Headers({ 'Content-Type': 'application/json', token }), 33 | credentials: 'same-origin', 34 | body: JSON.stringify(data) 35 | }); 36 | 37 | if (res.error) { 38 | throw error; 39 | } 40 | 41 | return res.json(); 42 | }; 43 | 44 | export const toDateTime = (secs) => { 45 | var t = new Date('1970-01-01T00:30:00Z'); // Unix epoch start. 46 | t.setSeconds(secs); 47 | return t; 48 | }; 49 | -------------------------------------------------------------------------------- /utils/pagination.ts: -------------------------------------------------------------------------------- 1 | export const PAGE_SIZE = 10; 2 | export const SCROLL_OFFSET_PX = 400; 3 | export const MAX_DEPTH = 10; -------------------------------------------------------------------------------- /utils/regex/punctuationRegex.js: -------------------------------------------------------------------------------- 1 | const punctuationRegex = /[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/g; 2 | 3 | export default punctuationRegex; -------------------------------------------------------------------------------- /utils/regex/validateEmailRegex.js: -------------------------------------------------------------------------------- 1 | function isEmail(email) { 2 | return /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test( 3 | email 4 | ); 5 | } 6 | 7 | export default isEmail; -------------------------------------------------------------------------------- /utils/stripe-client.js: -------------------------------------------------------------------------------- 1 | import { loadStripe } from '@stripe/stripe-js'; 2 | import { postData } from './helpers'; 3 | 4 | let stripePromise; 5 | 6 | export const getStripe = () => { 7 | if (!stripePromise) { 8 | stripePromise = loadStripe( 9 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_LIVE ?? 10 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY 11 | ); 12 | } 13 | 14 | return stripePromise; 15 | }; 16 | -------------------------------------------------------------------------------- /utils/stripe.js: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | 3 | export const stripe = new Stripe( 4 | process.env.STRIPE_SECRET_KEY_LIVE ?? process.env.STRIPE_SECRET_KEY, 5 | { 6 | // https://github.com/stripe/stripe-node#configuration 7 | apiVersion: '2020-08-27', 8 | // Register this as an official Stripe plugin. 9 | // https://stripe.com/docs/building-plugins#setappinfo 10 | appInfo: { 11 | name: 'Next.js Subscription Starter', 12 | version: '0.1.0' 13 | } 14 | } 15 | ); 16 | -------------------------------------------------------------------------------- /utils/supabase-admin.js: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | 3 | export const supabaseAdmin = createClient( 4 | process.env.NEXT_PUBLIC_SUPABASE_URL, 5 | process.env.SUPABASE_SERVICE_ROLE_KEY 6 | ); 7 | 8 | export const getUser = async (token) => { 9 | const { data, error } = await supabaseAdmin.auth.api.getUser(token); 10 | 11 | if (error) { 12 | throw error; 13 | } 14 | 15 | return data; 16 | }; 17 | -------------------------------------------------------------------------------- /utils/types.ts: -------------------------------------------------------------------------------- 1 | import { definitions } from '../types/supabase'; 2 | 3 | export interface CommentType { 4 | id: number; 5 | slug: string; 6 | title: string; 7 | content: string; 8 | authorId: string; 9 | parentId: number; 10 | createdAt: string; 11 | isPublished: boolean; 12 | updatedAt: string; 13 | author: definitions['users']; 14 | isPinned: boolean; 15 | responsesCount: number; 16 | responses: CommentType[]; 17 | parent?: CommentType; 18 | live: boolean; 19 | depth: number; 20 | justAuthored?: boolean; 21 | continueThread?: boolean; 22 | highlight?: boolean; 23 | isDeleted: boolean; 24 | isApproved: boolean; 25 | totalChildrenCount?: number; 26 | pageIndex?: number; 27 | path: number[]; 28 | votes: number; 29 | upvotes: number; 30 | downvotes: number; 31 | userVoteValue: number; 32 | pathVotesRecent: number[]; 33 | pathLeastRecent: number[]; 34 | pathMostRecent: number[]; 35 | } 36 | 37 | export interface User { 38 | handle?: string; 39 | name?: string; 40 | role?: any; 41 | id: string; 42 | image?: string; 43 | } -------------------------------------------------------------------------------- /utils/use-modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, createContext, useContext } from 'react'; 2 | import Portal from '@reach/portal'; 3 | type key = string; 4 | 5 | interface ModalContext { 6 | isOpen: boolean; 7 | openKeys: key[] | null; 8 | open: (key: key) => void; 9 | close: (key: key) => void; 10 | } 11 | 12 | interface ModalProviderProps { 13 | children?: React.ReactNode; 14 | [propName: string]: any; 15 | } 16 | 17 | const ModalContext = createContext({ 18 | isOpen: false, 19 | openKeys: [], 20 | open: () => null, 21 | close: () => null, 22 | }); 23 | 24 | const ModalElementsMap = new Map(); 25 | const BASE_Z_INDEX = 999999; 26 | 27 | const ModalProvider = (props: ModalProviderProps): JSX.Element => { 28 | // The top modal has the last key. 29 | const [openKeys, setOpenKeys] = useState([]); 30 | 31 | function handleOpen(key: key): void { 32 | if (!openKeys.includes(key)) { 33 | setOpenKeys((keys) => [...keys, key]); 34 | } 35 | } 36 | 37 | function handleClose(): void { 38 | setOpenKeys((keys) => keys.slice(0, -1)); 39 | } 40 | 41 | const value: ModalContext = { 42 | isOpen: openKeys.length > 0, 43 | openKeys, 44 | open: handleOpen, 45 | close: handleClose, 46 | }; 47 | 48 | return ( 49 | 50 | {props.children} 51 | {openKeys.map((key, index) => { 52 | const ReactElement = ModalElementsMap.get(key); 53 | 54 | if (!ReactElement) return null; 55 | 56 | return ( 57 | 58 |
59 | 60 |
61 |
62 | ); 63 | })} 64 |
65 | ); 66 | }; 67 | 68 | interface UseModalProps { 69 | [key: string]: React.ElementType; 70 | } 71 | 72 | const useModal = (props?: UseModalProps | key): ModalContext => { 73 | const context = useContext(ModalContext); 74 | if (!props) { 75 | return context; 76 | } 77 | 78 | if (typeof props === 'string') { 79 | const key = props; 80 | 81 | if (!ModalElementsMap.get(key)) { 82 | throw new Error(`Key ${key} does not exist.`); 83 | } 84 | 85 | return { ...context, open: () => context.open(key), close: () => context.close(key) }; 86 | } 87 | 88 | const elements = Object.entries(props); 89 | 90 | elements.forEach(([key, element]) => { 91 | ModalElementsMap.set(key, element); 92 | }); 93 | 94 | return context; 95 | }; 96 | 97 | export { ModalProvider, useModal }; 98 | // const {open, close} = useModal('sign_in_modal', SignInModalElement); -------------------------------------------------------------------------------- /utils/use-user.tsx: -------------------------------------------------------------------------------- 1 | import { Session, SupabaseClient, User } from '@supabase/supabase-js'; 2 | import { createContext, useContext, useEffect, useState } from 'react'; 3 | import useSWR from 'swr'; 4 | import { supabase } from './supabase-client'; 5 | import { definitions } from '../types/supabase'; 6 | 7 | interface AuthSessionProps { 8 | user: User | null; 9 | session: Session | null; 10 | profile?: definitions['users'] | null; 11 | loading: boolean; 12 | refresh: any; 13 | } 14 | const UserContext = createContext({ 15 | user: null, 16 | session: null, 17 | profile: null, 18 | loading: true, 19 | refresh: null, 20 | }); 21 | 22 | interface Props { 23 | supabaseClient: SupabaseClient; 24 | [propName: string]: any; 25 | } 26 | 27 | export const UserContextProvider = (props: Props): JSX.Element => { 28 | const { supabaseClient } = props; 29 | const [session, setSession] = useState(null); 30 | const [user, setUser] = useState(null); 31 | 32 | const { data: profile, error, isValidating, mutate } = useSWR( 33 | user?.id ? ['user_data', user.id] : null, 34 | async (_, userId) => 35 | supabase 36 | .from('users') 37 | .select('*') 38 | .eq('id', userId) 39 | .single() 40 | .then(({ data, error }) => { 41 | if (error) throw error; 42 | return data as definitions['users']; 43 | }), 44 | { revalidateOnFocus: false } 45 | ); 46 | 47 | if (error) { 48 | console.log(error); 49 | } 50 | 51 | useEffect(() => { 52 | const session = supabaseClient.auth.session(); 53 | 54 | if (session) { 55 | setSession(session); 56 | setUser(session?.user ?? null); 57 | } 58 | 59 | const { data: authListener, error } = supabaseClient.auth.onAuthStateChange( 60 | async (event, session) => { 61 | setSession(session); 62 | setUser(session?.user ?? null); 63 | } 64 | ); 65 | 66 | if (error) { 67 | throw error; 68 | } 69 | 70 | return () => { 71 | authListener!.unsubscribe(); 72 | }; 73 | 74 | // eslint-disable-next-line react-hooks/exhaustive-deps 75 | }, []); 76 | 77 | const loading = !session || !user || isValidating; 78 | 79 | const value = { 80 | session, 81 | user, 82 | profile, 83 | loading, 84 | refresh: mutate, 85 | }; 86 | 87 | return ; 88 | }; 89 | 90 | export const useUser = (): AuthSessionProps => { 91 | const context = useContext(UserContext); 92 | if (context === undefined) { 93 | throw new Error(`useUser must be used within a UserContextProvider.`); 94 | } 95 | return context; 96 | }; -------------------------------------------------------------------------------- /utils/useUser.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, createContext, useContext } from 'react'; 2 | import { supabase } from './supabase-client'; 3 | 4 | export const UserContext = createContext(); 5 | 6 | export const UserContextProvider = (props) => { 7 | const [userLoaded, setUserLoaded] = useState(false); 8 | const [session, setSession] = useState(null); 9 | const [user, setUser] = useState(null); 10 | const [userDetails, setUserDetails] = useState(null); 11 | const [subscription, setSubscription] = useState(null); 12 | 13 | useEffect(() => { 14 | const session = supabase.auth.session(); 15 | setSession(session); 16 | setUser(session?.user ?? null); 17 | const { data: authListener } = supabase.auth.onAuthStateChange( 18 | async (event, session) => { 19 | setSession(session); 20 | setUser(session?.user ?? null); 21 | } 22 | ); 23 | 24 | return () => { 25 | authListener.unsubscribe(); 26 | }; 27 | }, []); 28 | 29 | const getUserDetails = () => supabase.from('users').select('*').single(); 30 | const getSubscription = () => 31 | supabase 32 | .from('subscriptions') 33 | .select('*, prices(*, products(*))') 34 | .in('status', ['trialing', 'active']) 35 | .single(); 36 | 37 | useEffect(() => { 38 | if (user) { 39 | Promise.allSettled([getUserDetails(), getSubscription()]).then( 40 | (results) => { 41 | setUserDetails(results[0].value.data); 42 | setSubscription(results[1].value.data); 43 | setUserLoaded(true); 44 | } 45 | ); 46 | } 47 | }, [user]); 48 | 49 | const value = { 50 | session, 51 | user, 52 | profile: userDetails, 53 | userLoaded, 54 | subscription, 55 | signIn: (options) => supabase.auth.signIn(options), 56 | signUp: (options) => supabase.auth.signUp(options), 57 | signOut: () => { 58 | setUserDetails(null); 59 | setSubscription(null); 60 | return supabase.auth.signOut(); 61 | } 62 | }; 63 | return ; 64 | }; 65 | 66 | export const useUser = () => { 67 | const context = useContext(UserContext); 68 | if (context === undefined) { 69 | throw new Error(`useUser must be used within a UserContextProvider.`); 70 | } 71 | return context; 72 | }; 73 | --------------------------------------------------------------------------------