├── .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 |
6 |
7 |
8 |
Gardens
9 |
10 |
15 |
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 (
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 |
32 |
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 |
52 | )
53 | }
54 | );
55 | let currentGroupTitle = group ? group.job_group : 'General';
56 | groupArray.push(currentGroupTitle);
57 | return (
58 |
59 | {itemElements}
60 | )
61 | }
62 | );
63 |
64 |
65 | return (
66 | <>
67 |
68 |
70 |
71 |
72 |
77 |
78 | {listFlows}
79 |
80 |
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 |
36 | )}) : null
37 |
38 | let currentGroupTitle = group ? group : 'Default';
39 |
40 |
41 | if (currentGroupTitle != 'Default' && currentGroupTitle){
42 | groupArray.push(currentGroupTitle);
43 |
44 | return (
45 |
48 | {itemElements}
49 | )
50 | }
51 |
52 |
53 | }
54 | );
55 |
56 | return (
57 | <>
58 |
59 |
62 |
63 |
64 |
69 |
70 | {listListings}
71 |
72 |
73 | {/*
74 | */}
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 |
59 | )
60 | }
61 | );
62 |
63 |
64 |
65 | return (
66 | <>
67 |
68 |
70 |
71 |
72 |
73 |
74 |
75 | {(sortedItemArray.length > 0) ? (listFlows) : (
76 |
77 |
You don't have any private or public guides yet.
78 |
Press "New guide" to start writing!
)}
79 |
87 |
88 |
89 |
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 |
17 |
{
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 |
30 |
31 |
{
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 |
40 |
41 |
42 |
43 |
Inputs required
44 |
45 | {newFlowState.inputs.map((input, index) => {
46 | return
47 | })}
48 |
49 |
56 |
57 |
58 |
59 |
Instruction steps
60 |
61 | {newFlowState.steps.map((step, index) => {
62 | return (
63 |
64 | )
65 | })}
66 |
67 |
74 |
75 |
76 |
End result
77 |
78 |
79 | {newFlowService.setChecked(!newFlowState.isPrivate)}} type="checkbox"/>
80 | Make private?
81 |
82 |
90 |
91 |
92 |
93 |
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 ()
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 |
41 | )
42 | }
43 | )
44 |
45 |
46 | return (
47 | <>
48 |
49 |
51 |
52 |
53 | {/*
*/}
58 |
59 |
60 | Get {currentOutput.output.toLowerCase()} by following these flows
61 |
62 | {itemElements}
63 |
64 |
65 |
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) => {
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 |
31 |
Hi there!
32 |
33 |
34 |
35 |
36 | {message.content && (
37 |
44 | {message.content}
45 |
46 | )}
47 |
64 |
65 |
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([])
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 |
42 |
{
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
58 |
61 |
65 |
66 |
67 | })}
68 |
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 |
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 ()
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 |
48 | {itemElements}
49 | )
50 | }
51 | );
52 |
53 | return (
54 | <>
55 |
56 |
59 |
60 |
61 |
66 |
67 | {listJobs}
68 |
69 |
70 | {/*
71 | */}
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 | )
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 (
75 | )
76 | });
77 |
78 | if (category && (category.length != 0)){
79 | return (
80 |
83 | {itemElements}
84 | )
85 | }
86 |
87 |
88 | }
89 | );
90 |
91 |
92 | return (
93 | <>
94 |
95 |
98 |
99 |
100 |
105 |
106 |
107 | {listTools}
108 |
109 |
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 | )
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 |
56 | {itemElements}
57 | )
58 | }
59 |
60 |
61 | }
62 | );
63 |
64 | return (
65 | <>
66 |
67 |
70 |
71 |
72 |
77 |
78 | {listJobs}
79 |
80 |
81 | {/*
82 | */}
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 |
--------------------------------------------------------------------------------
/public/nextjs.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joingardens/gardens/99eb6816cceeb30ae9bcedde4f7abbb14db7f245/public/og.png
--------------------------------------------------------------------------------
/public/stripe.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
--------------------------------------------------------------------------------
/public/vercel-deploy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joingardens/gardens/99eb6816cceeb30ae9bcedde4f7abbb14db7f245/public/vercel-deploy.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------