├── .air.toml
├── .dockerignore
├── .env.dev
├── .gitattributes
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .kamal
└── hooks
│ ├── post-deploy.sample
│ ├── post-traefik-reboot.sample
│ ├── pre-build.sample
│ ├── pre-connect.sample
│ ├── pre-deploy.sample
│ └── pre-traefik-reboot.sample
├── .vscode
├── extensions.json
├── settings.json
└── tailwind.json
├── Dockerfile
├── LICENSE
├── Makefile
├── Procfile
├── README.md
├── build.mjs
├── cmd
├── seed
│ └── main.go
├── web
│ └── main.go
└── worker
│ └── main.go
├── config
├── config.go
├── config.yaml
├── config_test.go
└── firewalls
│ ├── accessories.sh
│ ├── web-app.sh
│ └── worker.sh
├── deploy
└── kamal
│ └── deploy.yml
├── docker-compose.yml
├── e2e_tests
├── config.js
├── package-lock.json
├── package.json
├── playwright.config.ts
└── tests
│ ├── goship.spec.ts
│ └── helper.ts
├── ent
├── client.go
├── emailsubscription.go
├── emailsubscription
│ ├── emailsubscription.go
│ └── where.go
├── emailsubscription_create.go
├── emailsubscription_delete.go
├── emailsubscription_query.go
├── emailsubscription_update.go
├── emailsubscriptiontype.go
├── emailsubscriptiontype
│ ├── emailsubscriptiontype.go
│ └── where.go
├── emailsubscriptiontype_create.go
├── emailsubscriptiontype_delete.go
├── emailsubscriptiontype_query.go
├── emailsubscriptiontype_update.go
├── emojis.go
├── emojis
│ ├── emojis.go
│ └── where.go
├── emojis_create.go
├── emojis_delete.go
├── emojis_query.go
├── emojis_update.go
├── ent.go
├── enttest
│ └── enttest.go
├── fcmsubscriptions.go
├── fcmsubscriptions
│ ├── fcmsubscriptions.go
│ └── where.go
├── fcmsubscriptions_create.go
├── fcmsubscriptions_delete.go
├── fcmsubscriptions_query.go
├── fcmsubscriptions_update.go
├── filestorage.go
├── filestorage
│ ├── filestorage.go
│ └── where.go
├── filestorage_create.go
├── filestorage_delete.go
├── filestorage_query.go
├── filestorage_update.go
├── generate.go
├── hook
│ ├── custom_hook.go
│ └── hook.go
├── image.go
├── image
│ ├── image.go
│ └── where.go
├── image_create.go
├── image_delete.go
├── image_query.go
├── image_update.go
├── imagesize.go
├── imagesize
│ ├── imagesize.go
│ └── where.go
├── imagesize_create.go
├── imagesize_delete.go
├── imagesize_query.go
├── imagesize_update.go
├── invitation.go
├── invitation
│ ├── invitation.go
│ └── where.go
├── invitation_create.go
├── invitation_delete.go
├── invitation_query.go
├── invitation_update.go
├── lastseenonline.go
├── lastseenonline
│ ├── lastseenonline.go
│ └── where.go
├── lastseenonline_create.go
├── lastseenonline_delete.go
├── lastseenonline_query.go
├── lastseenonline_update.go
├── migrate
│ ├── migrate.go
│ ├── migrations
│ │ ├── 20240903200944.sql
│ │ ├── 20240907154749.sql
│ │ ├── 20240907155651.sql
│ │ ├── 20240907160557.sql
│ │ ├── 20240907214903.sql
│ │ └── atlas.sum
│ └── schema.go
├── monthlysubscription.go
├── monthlysubscription
│ ├── monthlysubscription.go
│ └── where.go
├── monthlysubscription_create.go
├── monthlysubscription_delete.go
├── monthlysubscription_query.go
├── monthlysubscription_update.go
├── mutation.go
├── notification.go
├── notification
│ ├── notification.go
│ └── where.go
├── notification_create.go
├── notification_delete.go
├── notification_query.go
├── notification_update.go
├── notificationpermission.go
├── notificationpermission
│ ├── notificationpermission.go
│ └── where.go
├── notificationpermission_create.go
├── notificationpermission_delete.go
├── notificationpermission_query.go
├── notificationpermission_update.go
├── notificationtime.go
├── notificationtime
│ ├── notificationtime.go
│ └── where.go
├── notificationtime_create.go
├── notificationtime_delete.go
├── notificationtime_query.go
├── notificationtime_update.go
├── passwordtoken.go
├── passwordtoken
│ ├── passwordtoken.go
│ └── where.go
├── passwordtoken_create.go
├── passwordtoken_delete.go
├── passwordtoken_query.go
├── passwordtoken_update.go
├── phoneverificationcode.go
├── phoneverificationcode
│ ├── phoneverificationcode.go
│ └── where.go
├── phoneverificationcode_create.go
├── phoneverificationcode_delete.go
├── phoneverificationcode_query.go
├── phoneverificationcode_update.go
├── predicate
│ └── predicate.go
├── profile.go
├── profile
│ ├── profile.go
│ └── where.go
├── profile_create.go
├── profile_delete.go
├── profile_query.go
├── profile_update.go
├── pwapushsubscription.go
├── pwapushsubscription
│ ├── pwapushsubscription.go
│ └── where.go
├── pwapushsubscription_create.go
├── pwapushsubscription_delete.go
├── pwapushsubscription_query.go
├── pwapushsubscription_update.go
├── runtime.go
├── runtime
│ └── runtime.go
├── schema
│ ├── emailsubscription.go
│ ├── emailsubscriptiontype.go
│ ├── emojis.go
│ ├── fcmsubscriptions.go
│ ├── filestorage.go
│ ├── image.go
│ ├── imagesize.go
│ ├── invitation.go
│ ├── lastseenonline.go
│ ├── mixin.go
│ ├── monthlysubscription.go
│ ├── notification.go
│ ├── notificationpermission.go
│ ├── notificationtime.go
│ ├── passwordtoken.go
│ ├── phoneverificationcode.go
│ ├── profile.go
│ ├── pwapushsubscription.go
│ ├── sentemail.go
│ └── user.go
├── sentemail.go
├── sentemail
│ ├── sentemail.go
│ └── where.go
├── sentemail_create.go
├── sentemail_delete.go
├── sentemail_query.go
├── sentemail_update.go
├── tx.go
├── user.go
├── user
│ ├── user.go
│ └── where.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go
├── entrypoint.sh
├── go.mod
├── go.sum
├── javascript
├── svelte
│ ├── components
│ │ ├── MultiSelectComponent.svelte
│ │ ├── NotificationPermissions.svelte
│ │ ├── PhoneNumberPicker.svelte
│ │ ├── PhotoUploader.svelte
│ │ ├── PwaInstallButton.svelte
│ │ ├── PwaSubscribePushChecker.svelte
│ │ ├── SingleSelect.svelte
│ │ ├── SvelteTodoComponent.svelte
│ │ ├── ThemeToggle.svelte
│ │ ├── notifications
│ │ │ ├── EmailSubscribe.svelte
│ │ │ ├── IOSSubscribePush.svelte
│ │ │ ├── IndividualNotificationPermission.svelte
│ │ │ ├── PermissionButton.svelte
│ │ │ ├── PwaSubscribePush.svelte
│ │ │ ├── SmsSubscribe.svelte
│ │ │ ├── _NotificationPermission.svelte
│ │ │ └── icons
│ │ │ │ ├── EmailDisabledIcon.svelte
│ │ │ │ ├── EmailEnabledIcon.svelte
│ │ │ │ ├── LoadingSpinner.svelte
│ │ │ │ ├── PushDisabledIcon.svelte
│ │ │ │ ├── PushEnabledIcon.svelte
│ │ │ │ ├── SmsDisabledIcon.svelte
│ │ │ │ └── SmsEnabledIcon.svelte
│ │ └── utils.js
│ └── main.js
└── vanilla
│ ├── cal_heatmap.js
│ ├── load_scripts_and_styles.js
│ ├── main.js
│ └── test_quiz.js
├── package-lock.json
├── package.json
├── pgvector-image
├── Dockerfile
└── init_pgvector.sh
├── pkg
├── context
│ ├── context.go
│ └── context_test.go
├── controller
│ ├── controller.go
│ ├── controller_test.go
│ ├── form.go
│ ├── form_test.go
│ ├── page.go
│ ├── page_test.go
│ ├── pager.go
│ └── pager_test.go
├── domain
│ ├── constants.go
│ ├── enum.go
│ ├── maps.go
│ └── struct.go
├── funcmap
│ ├── funcmap.go
│ └── funcmap_test.go
├── htmx
│ ├── htmx.go
│ └── htmx_test.go
├── middleware
│ ├── auth.go
│ ├── auth_test.go
│ ├── cache.go
│ ├── cache_test.go
│ ├── device.go
│ ├── entity.go
│ ├── entity_test.go
│ ├── lastseenonline.go
│ ├── log.go
│ ├── log_test.go
│ ├── middleware_test.go
│ ├── onboarding.go
│ └── sentry.go
├── repos
│ ├── emailsmanager
│ │ ├── emailsubscriptionrepo.go
│ │ ├── emailsubscriptionrepo_test.go
│ │ ├── update_email.go
│ │ └── update_email_test.go
│ ├── mailer
│ │ ├── mailer.go
│ │ ├── resend.go
│ │ └── smtp.go
│ ├── msg
│ │ ├── msg.go
│ │ └── msg_test.go
│ ├── notifierrepo
│ │ ├── notificationstoragerepo.go
│ │ ├── notificationstoragerepo_test.go
│ │ ├── notifier.go
│ │ ├── notifier_test.go
│ │ ├── permissions.go
│ │ ├── permissions_test.go
│ │ ├── planned_notifications.go
│ │ ├── planned_notifications_test.go
│ │ ├── push_notifications_fcm.go
│ │ ├── push_notifications_fcm_test.go
│ │ ├── push_notifications_pwa.go
│ │ └── sms.go
│ ├── profilerepo
│ │ ├── profilerepo.go
│ │ └── profilerepo_test.go
│ ├── pubsub
│ │ └── pubsub.go
│ ├── storage
│ │ ├── mocks.go
│ │ └── storagerepo.go
│ └── subscriptions
│ │ ├── subscriptions.go
│ │ └── subscriptions_test.go
├── routing
│ ├── routenames
│ │ └── routenames.go
│ └── routes
│ │ ├── about.go
│ │ ├── about_test.go
│ │ ├── clear_site_cookie.go
│ │ ├── contact.go
│ │ ├── delete_account.go
│ │ ├── docs.go
│ │ ├── email_subscribe.go
│ │ ├── error.go
│ │ ├── forgot_password.go
│ │ ├── healthcheck.go
│ │ ├── helpers.go
│ │ ├── home.go
│ │ ├── home_feed.go
│ │ ├── install_app.go
│ │ ├── landing.go
│ │ ├── login.go
│ │ ├── logout.go
│ │ ├── notifications.go
│ │ ├── payments.go
│ │ ├── preferences.go
│ │ ├── privacy.go
│ │ ├── profile.go
│ │ ├── profile_photo.go
│ │ ├── push_notifs.go
│ │ ├── realtime.go
│ │ ├── register.go
│ │ ├── register_test.go
│ │ ├── reset_password.go
│ │ ├── router.go
│ │ ├── routes_test.go
│ │ ├── upload_photo.go
│ │ ├── verify_email.go
│ │ └── verify_email_subscription.go
├── services
│ ├── auth.go
│ ├── auth_test.go
│ ├── cache.go
│ ├── cache_test.go
│ ├── container.go
│ ├── container_test.go
│ ├── services_test.go
│ ├── tasks.go
│ ├── tasks_test.go
│ ├── validator.go
│ └── validator_test.go
├── tasks
│ ├── mail.go
│ ├── notifications.go
│ └── subscriptions.go
├── tests
│ └── tests.go
└── types
│ ├── about.go
│ ├── committed.go
│ ├── contact.go
│ ├── email_subscribe.go
│ ├── emails.go
│ ├── forgot_password.go
│ ├── home.go
│ ├── home_feed.go
│ ├── invitations.go
│ ├── landing_page.go
│ ├── login.go
│ ├── notifications.go
│ ├── page_data.go
│ ├── payments.go
│ ├── preferences.go
│ ├── profile.go
│ ├── register.go
│ ├── reset_password.go
│ └── search.go
├── pwabuilder-ios-wrapper
├── next-steps.html
└── src
│ ├── .gitignore
│ ├── Cherie.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Cherie.xcscheme
│ ├── Cherie.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ ├── Cherie
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── 100.png
│ │ │ ├── 1024.png
│ │ │ ├── 114.png
│ │ │ ├── 120.png
│ │ │ ├── 128.png
│ │ │ ├── 144.png
│ │ │ ├── 152.png
│ │ │ ├── 16.png
│ │ │ ├── 167.png
│ │ │ ├── 180.png
│ │ │ ├── 20.png
│ │ │ ├── 256.png
│ │ │ ├── 29.png
│ │ │ ├── 32.png
│ │ │ ├── 40.png
│ │ │ ├── 50.png
│ │ │ ├── 512.png
│ │ │ ├── 57.png
│ │ │ ├── 58.png
│ │ │ ├── 60.png
│ │ │ ├── 64.png
│ │ │ ├── 72.png
│ │ │ ├── 76.png
│ │ │ ├── 80.png
│ │ │ ├── 87.png
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── Image.imageset
│ │ │ └── Contents.json
│ │ └── LaunchIcon.imageset
│ │ │ ├── Contents.json
│ │ │ ├── launch-128.png
│ │ │ ├── launch-192.png
│ │ │ └── launch-64.png
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Entitlements
│ │ ├── .gitignore
│ │ └── Entitlements.plist
│ ├── GoogleService-Info.plist
│ ├── IAP.swift
│ ├── Info.plist
│ ├── Printer.swift
│ ├── PushNotifications.swift
│ ├── SceneDelegate.swift
│ ├── Settings.swift
│ ├── ViewController.swift
│ └── WebView.swift
│ ├── LICENSE
│ ├── Podfile
│ ├── launch-128.png
│ ├── launch-192.png
│ ├── launch-256.png
│ ├── launch-512.png
│ └── launch-64.png
├── scripts
├── regen_logo_images.py
└── requirements.txt
├── seeder
├── seeder.go
└── seeder_test.go
├── service-worker.js
├── static
├── cherie_pwa_apple-icon-180.png
├── cherie_pwa_manifest-icon-192.maskable.png
├── cherie_pwa_manifest-icon-512.maskable.png
├── cherie_pwa_manifest-icon-96.maskable.png
├── favicon.png
├── icon.png
├── main.css
├── main.css.map
├── main.js
├── main.js.map
├── manifest.json
├── meta_main.json
├── meta_svelte_bundle.json
├── meta_vanilla_bundle.json
├── styles_bundle.css
├── svelte_bundle.css
├── svelte_bundle.css.map
├── svelte_bundle.js
├── svelte_bundle.js.map
├── vanilla_bundle.js
└── vanilla_bundle.js.map
├── styles
├── styles.css
└── tailwind_components.css
├── tailwind.config.js
├── templates
├── components
│ ├── accordion.templ
│ ├── accordion_templ.go
│ ├── auth.templ
│ ├── auth_templ.go
│ ├── bottom_nav.templ
│ ├── bottom_nav_templ.go
│ ├── core.templ
│ ├── core_templ.go
│ ├── documentation.templ
│ ├── documentation_templ.go
│ ├── drawer.templ
│ ├── drawer_templ.go
│ ├── empty_page_msg.templ
│ ├── empty_page_msg_templ.go
│ ├── forms.templ
│ ├── forms_templ.go
│ ├── heatmap.templ
│ ├── heatmap_templ.go
│ ├── icons.templ
│ ├── icons_templ.go
│ ├── loading.templ
│ ├── loading_templ.go
│ ├── logos.templ
│ ├── logos_templ.go
│ ├── messages.templ
│ ├── messages_templ.go
│ ├── navbar.templ
│ ├── navbar_templ.go
│ ├── payments.templ
│ ├── payments_templ.go
│ ├── permissions.templ
│ ├── permissions_templ.go
│ ├── prev_nav.templ
│ ├── prev_nav_templ.go
│ ├── profile.templ
│ ├── profile_templ.go
│ ├── pwa_install.templ
│ ├── pwa_install_templ.go
│ ├── sidebar.templ
│ ├── sidebar_templ.go
│ ├── theme_toggle.templ
│ ├── theme_toggle_templ.go
│ ├── tooltip.templ
│ ├── tooltip_templ.go
│ ├── top_banner.templ
│ └── top_banner_templ.go
├── emails
│ ├── password_reset.templ
│ ├── password_reset_templ.go
│ ├── registration_confirmation.templ
│ ├── registration_confirmation_templ.go
│ ├── subscription_confirmation.templ
│ ├── subscription_confirmation_templ.go
│ ├── test.templ
│ ├── test_templ.go
│ ├── update.templ
│ └── update_templ.go
├── helpers
│ ├── helpers.templ
│ └── helpers_templ.go
├── layouts
│ ├── auth.templ
│ ├── auth_templ.go
│ ├── documentation.templ
│ ├── documentation_templ.go
│ ├── email.templ
│ ├── email_templ.go
│ ├── landing_page.templ
│ ├── landing_page_templ.go
│ ├── main.templ
│ └── main_templ.go
├── pages
│ ├── about.templ
│ ├── about_templ.go
│ ├── contact.templ
│ ├── contact_templ.go
│ ├── delete_account.templ
│ ├── delete_account_templ.go
│ ├── docs_architecture.templ
│ ├── docs_architecture_templ.go
│ ├── docs_emails.templ
│ ├── docs_emails_templ.go
│ ├── documentation.templ
│ ├── documentation_templ.go
│ ├── email_subscribe.templ
│ ├── email_subscribe_templ.go
│ ├── error.templ
│ ├── error_templ.go
│ ├── forgot_password.templ
│ ├── forgot_password_templ.go
│ ├── healthcheck.templ
│ ├── healthcheck_templ.go
│ ├── home.templ
│ ├── home_feed.templ
│ ├── home_feed_templ.go
│ ├── home_templ.go
│ ├── install_app.templ
│ ├── install_app_templ.go
│ ├── invitations.templ
│ ├── invitations_templ.go
│ ├── landing_page.templ
│ ├── landing_page_templ.go
│ ├── login.templ
│ ├── login_templ.go
│ ├── notifications.templ
│ ├── notifications_templ.go
│ ├── payments.templ
│ ├── payments_templ.go
│ ├── phone.templ
│ ├── phone_templ.go
│ ├── preferences.templ
│ ├── preferences_templ.go
│ ├── privacy_policy.templ
│ ├── privacy_policy_templ.go
│ ├── profile.templ
│ ├── profile_templ.go
│ ├── register.templ
│ ├── register_templ.go
│ ├── reset_password.templ
│ └── reset_password_templ.go
└── templates.go
├── testdata
└── photos
│ ├── 1.jpg
│ └── 2.jpg
└── tsconfig.json
/.air.toml:
--------------------------------------------------------------------------------
1 | root = "."
2 | testdata_dir = "testdata"
3 | tmp_dir = "tmp"
4 |
5 | [build]
6 | exclude_dir = [
7 | "assets",
8 | "tmp",
9 | "vendor",
10 | "testdata",
11 | "e2e_tests",
12 | "node_modules",
13 | "javascript",
14 | "pwabuilder-ios-wrapper",
15 | "pwabuilder-android-wrapper",
16 | "schemaspy-output",
17 | "scripts",
18 |
19 | ]
20 | args_bin = []
21 | bin = "./tmp/main"
22 | cmd = "templ generate && go build -o ./tmp/main cmd/web/main.go"
23 | delay = 1000
24 | exclude_file = []
25 | exclude_regex = [".*_templ.go", "_test.go"]
26 | exclude_unchanged = false
27 | follow_symlink = false
28 | full_bin = ""
29 | include_dir = []
30 | include_ext = ["go", "tpl", "tmpl", "templ", "html"]
31 | include_file = []
32 | kill_delay = "0s"
33 | log = "build-errors.log"
34 | poll = false
35 | poll_interval = 0
36 | post_cmd = []
37 | pre_cmd = []
38 | rerun = false
39 | rerun_delay = 500
40 | send_interrupt = false
41 | stop_on_error = false
42 |
43 | [color]
44 | app = ""
45 | build = "yellow"
46 | main = "magenta"
47 | runner = "green"
48 | watcher = "cyan"
49 |
50 | [log]
51 | main_only = false
52 | time = false
53 |
54 | [misc]
55 | clean_on_exit = false
56 |
57 | [screen]
58 | clear_on_rebuild = false
59 | keep_scroll = true
60 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .env*
2 | .air*
3 | .gitignore
4 | *.md
5 | Dockerfile
6 | LICENSE
7 | Makefile
8 | tailwind*
9 | *.csv
10 | .github
11 | .kamal
12 | schemaspy*
13 | testdata
14 | tmp
--------------------------------------------------------------------------------
/.env.dev:
--------------------------------------------------------------------------------
1 | #######################################
2 | # NOTE:
3 | # For production, copy-paste this file to `./.env` and make sure not to commit it
4 | # to your git repo...except if you want to one day regret it lol.
5 | #
6 | # If some env vars are not being pushed by kamal to the containers, it is likely because they
7 | # have not been added to the "secret" section of the kamal config (./deploy.yaml)
8 | #######################################
9 |
10 | KAMAL_REGISTRY_USERNAME=username
11 | KAMAL_REGISTRY_PASSWORD=ghp_123
12 |
13 | # TODO: set the below env var
14 | PAGODA_APP_ENCRYPTIONKEY=abcdefghijklmnopqrstuv
15 |
16 | # PAGODA_DATABASE_HOSTNAME=
17 | # PAGODA_DATABASE_USER=
18 | # PAGODA_DATABASE_PASSWORD=
19 | PAGODA_DATABASE_DATABASE=postgres
20 |
21 | PAGODA_MAIL_RESENDAPIKEY=re_123
22 |
23 | PAGODA_STORAGE_S3ACCESSKEY=123
24 | PAGODA_STORAGE_S3SECRETKEY=123
25 |
26 | # For DB Backups
27 | S3_ACCESS_KEY_ID=123
28 | S3_SECRET_ACCESS_KEY=123
29 |
30 | # Sentry
31 | PAGODA_APP_SENTRYDSN=FILL
32 |
33 | # VAPID
34 | PAGODA_APP_VAPIDPUBLICKEY=
35 | PAGODA_APP_VAPIDPRIVATEKEY=pZ-FILL
36 |
37 | # Stripe
38 | PAGODA_APP_PUBLICSTRIPEKEY=pk_
39 | PAGODA_APP_PRIVATESTRIPEKEY=sk_
40 | PAGODA_APP_STRIPEWEBHOOKSECRET=whsec_
41 |
42 | # E3Kit
43 | PAGODA_APP_E3KIT_APPID=2132131
44 | PAGODA_APP_E3KIT_APPKEYID=12313
45 | PAGODA_APP_E3KIT_APPKEY=123143
46 |
47 | # Generate an AEAD keyset using the AES256-GCM key template:
48 | # $ tinkey create-keyset --key-template AES256_GCM --out-format JSON --out tmp/keyset.json
49 | # Convert the Keyset to Base64
50 | # $ cat tmp/keyset.json | jq -c .key | base64
51 | PAGODA_APP_APPENCRYPTIONKEY="FILL"
52 |
53 | # Only for dev for docker-compose
54 | HOST_CACHE_PORT=6379
55 | HOST_DB_PORT=5432
56 | HOST_MAILPIT_HTTP_PORT=8025
57 | HOST_MAILPIT_SMTP_PORT=1025
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | static/* -diff
2 | *_templ.go -diff
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | # TODO: Earthly (which I've used before) can be used to create platform-agnostic CI tests,
4 | # in case we want to move away from Github seamlessly.
5 | on:
6 | push:
7 | branches: [main]
8 | pull_request:
9 | branches: [main]
10 |
11 | jobs:
12 | backend_tests:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 |
17 | - name: Set up Go
18 | uses: actions/setup-go@v3
19 | with:
20 | go-version: 1.22.2
21 |
22 | - uses: actions/cache@v3
23 | with:
24 | path: |
25 | ~/.cache/go-build
26 | ~/go/pkg/mod
27 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
28 | restore-keys: |
29 | ${{ runner.os }}-go-
30 |
31 | - name: Start containers
32 | run: |
33 | docker compose up -d
34 | sleep 3
35 |
36 | - name: Test
37 | run: go test -p 1 ./...
38 |
39 | # TODO: these were never set up in the CI
40 | # e2e_tests:
41 | # needs: backend_tests
42 | # runs-on: ubuntu-latest
43 | # steps:
44 | # - uses: actions/checkout@v3
45 |
46 | # - name: Set up Node.js
47 | # uses: actions/setup-node@v3
48 | # with:
49 | # node-version: 18
50 |
51 | # - name: Set up Go
52 | # uses: actions/setup-go@v3
53 | # with:
54 | # go-version: 1.21.3
55 |
56 | # - name: Check if app is running
57 | # run: |
58 | # until curl --output /dev/null --silent --head --fail http://localhost:8000; do
59 | # echo "Waiting for app to be up..."
60 | # sleep 5
61 | # done
62 |
63 | # - name: Install dependencies
64 | # run: npm ci
65 | # working-directory: ./e2e_tests
66 |
67 | # - name: Install Playwright Browsers
68 | # run: npx playwright install --with-deps
69 | # working-directory: ./e2e_tests
70 |
71 | # - name: Seed Database
72 | # run: make seed
73 |
74 | # - name: Run app
75 | # run: make watch
76 |
77 | # - name: Run Playwright tests
78 | # run: npx playwright test
79 | # working-directory: ./e2e_tests
80 |
81 | # - uses: actions/upload-artifact@v3
82 | # if: always()
83 | # with:
84 | # name: playwright-report
85 | # path: playwright-report/
86 | # retention-days: 1
87 |
--------------------------------------------------------------------------------
/.kamal/hooks/post-deploy.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # A sample post-deploy hook
4 | #
5 | # These environment variables are available:
6 | # KAMAL_RECORDED_AT
7 | # KAMAL_PERFORMER
8 | # KAMAL_VERSION
9 | # KAMAL_HOSTS
10 | # KAMAL_ROLE (if set)
11 | # KAMAL_DESTINATION (if set)
12 | # KAMAL_RUNTIME
13 |
14 | echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
15 |
--------------------------------------------------------------------------------
/.kamal/hooks/post-traefik-reboot.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | echo "Rebooted Traefik on $KAMAL_HOSTS"
4 |
--------------------------------------------------------------------------------
/.kamal/hooks/pre-build.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # A sample pre-build hook
4 | #
5 | # Checks:
6 | # 1. We have a clean checkout
7 | # 2. A remote is configured
8 | # 3. The branch has been pushed to the remote
9 | # 4. The version we are deploying matches the remote
10 | #
11 | # These environment variables are available:
12 | # KAMAL_RECORDED_AT
13 | # KAMAL_PERFORMER
14 | # KAMAL_VERSION
15 | # KAMAL_HOSTS
16 | # KAMAL_ROLE (if set)
17 | # KAMAL_DESTINATION (if set)
18 |
19 | if [ -n "$(git status --porcelain)" ]; then
20 | echo "Git checkout is not clean, aborting..." >&2
21 | git status --porcelain >&2
22 | exit 1
23 | fi
24 |
25 | first_remote=$(git remote)
26 |
27 | if [ -z "$first_remote" ]; then
28 | echo "No git remote set, aborting..." >&2
29 | exit 1
30 | fi
31 |
32 | current_branch=$(git branch --show-current)
33 |
34 | if [ -z "$current_branch" ]; then
35 | echo "Not on a git branch, aborting..." >&2
36 | exit 1
37 | fi
38 |
39 | remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
40 |
41 | if [ -z "$remote_head" ]; then
42 | echo "Branch not pushed to remote, aborting..." >&2
43 | exit 1
44 | fi
45 |
46 | if [ "$KAMAL_VERSION" != "$remote_head" ]; then
47 | echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
48 | exit 1
49 | fi
50 |
51 | exit 0
52 |
--------------------------------------------------------------------------------
/.kamal/hooks/pre-connect.sample:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # A sample pre-connect check
4 | #
5 | # Warms DNS before connecting to hosts in parallel
6 | #
7 | # These environment variables are available:
8 | # KAMAL_RECORDED_AT
9 | # KAMAL_PERFORMER
10 | # KAMAL_VERSION
11 | # KAMAL_HOSTS
12 | # KAMAL_ROLE (if set)
13 | # KAMAL_DESTINATION (if set)
14 | # KAMAL_RUNTIME
15 |
16 | hosts = ENV["KAMAL_HOSTS"].split(",")
17 | results = nil
18 | max = 3
19 |
20 | elapsed = Benchmark.realtime do
21 | results = hosts.map do |host|
22 | Thread.new do
23 | tries = 1
24 |
25 | begin
26 | Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
27 | rescue SocketError
28 | if tries < max
29 | puts "Retrying DNS warmup: #{host}"
30 | tries += 1
31 | sleep rand
32 | retry
33 | else
34 | puts "DNS warmup failed: #{host}"
35 | host
36 | end
37 | end
38 |
39 | tries
40 | end
41 | end.map(&:value)
42 | end
43 |
44 | retries = results.sum - hosts.size
45 | nopes = results.count { |r| r == max }
46 |
47 | puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
48 |
--------------------------------------------------------------------------------
/.kamal/hooks/pre-traefik-reboot.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | echo "Rebooting Traefik on $KAMAL_HOSTS..."
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "pwabuilder.pwa-studio",
4 | "a-h.templ",
5 | "michelemelluso.code-beautifier",
6 | "golang.go",
7 | "jinliming2.vscode-go-template",
8 | "yzhang.markdown-all-in-one",
9 | "esbenp.prettier-vscode",
10 | "1yib.svelte-bundle",
11 | "pivaszbs.svelte-autoimport",
12 | "svelte.svelte-vscode",
13 | "fivethree.vscode-svelte-snippets",
14 | "bradlc.vscode-tailwindcss"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "css.customData": [".vscode/tailwind.json"]
3 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 1: Build the Go application
2 | FROM golang:1.24.3-bullseye AS builder
3 |
4 | # Set the working directory inside the container
5 | WORKDIR /app
6 |
7 | # NOTE: Install necessary libraries for SQLite
8 | RUN apt-get update && apt-get install -y \
9 | gcc \
10 | musl-dev \
11 | sqlite3 \
12 | libsqlite3-dev
13 |
14 | # Copy Go modules and download dependencies
15 | COPY go.mod go.sum ./
16 | RUN go mod tidy
17 | RUN go mod download
18 |
19 | # Copy the source code
20 | COPY . .
21 |
22 | ENV CGO_ENABLED=1
23 | ENV GOOS=linux
24 | ENV GOARCH=amd64
25 |
26 | # Build the application
27 | RUN GOARCH=amd64 go build -ldflags="-s -w" -gcflags=all=-l -o /app/goship-web ./cmd/web/main.go
28 | RUN GOARCH=amd64 go build -ldflags="-s -w" -gcflags=all=-l -o /app/goship-worker ./cmd/worker/main.go
29 | RUN GOARCH=amd64 go build -ldflags="-s -w" -gcflags=all=-l -o /app/goship-seed ./cmd/seed/main.go
30 |
31 | # Install asynq tools
32 | RUN go install github.com/hibiken/asynq/tools/asynq@latest
33 |
34 | ################################################
35 | # Stage 2: Create a smaller runtime image
36 | ################################################
37 | FROM ubuntu:22.04
38 |
39 | # Install necessary packages
40 | RUN apt-get update && apt-get install -y \
41 | curl
42 |
43 | # Copy the compiled binaries from the builder image
44 | COPY --from=builder /app/goship-web /goship-web
45 | COPY --from=builder /app/goship-worker /goship-worker
46 | COPY --from=builder /app/goship-seed /goship-seed
47 |
48 | # Copy asynq tool
49 | COPY --from=builder /go/bin/asynq /usr/local/bin/
50 |
51 | # Copy the templates
52 | COPY templates/ /app/templates/
53 |
54 | # Optional: Bind to a TCP port (document the ports the application listens on)
55 | EXPOSE 8000
56 | EXPOSE 8080
57 |
58 | # Define an entrypoint script
59 | COPY entrypoint.sh /entrypoint.sh
60 | RUN chmod +x /entrypoint.sh
61 |
62 | COPY config/config.yaml .
63 | COPY service-worker.js /service-worker.js
64 | COPY static /static
65 |
66 | # Below is only used if you need to use PWABuilder to make a native Android app
67 | # RUN mkdir pwabuilder-android-wrapper
68 | # COPY pwabuilder-android-wrapper/assetlinks.json pwabuilder-android-wrapper/assetlinks.json
69 |
70 | ENTRYPOINT ["/entrypoint.sh"]
71 |
72 | # Clean up any unnecessary files
73 | RUN apt-get purge -y gcc musl-dev libsqlite3-dev && apt-get autoremove -y && apt-get clean
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Mike Stefanello
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | watch-js: make watch-js
2 | watch-go: make watch-go
3 | watch-css: make watch-css
4 | watch-go-worker: make worker
5 |
--------------------------------------------------------------------------------
/cmd/seed/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/services"
5 | "github.com/mikestefanello/pagoda/seeder"
6 | )
7 |
8 | func main() {
9 | c := services.NewContainer()
10 | seeder.SeedUsers(c.Config, c.ORM, true)
11 | c.Shutdown()
12 | }
13 |
--------------------------------------------------------------------------------
/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestGetConfig(t *testing.T) {
11 | _, err := GetConfig()
12 | require.NoError(t, err)
13 |
14 | cfg, err := GetConfig()
15 | require.NoError(t, err)
16 | assert.Equal(t, EnvLocal, cfg.App.Environment)
17 | }
18 |
--------------------------------------------------------------------------------
/config/firewalls/accessories.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This firewall script allows access to the following ports:
3 | # - 6379: Redis
4 | # - 8080: AsyncMon
5 | # - 22: SSH
6 |
7 | YOUR_LAPTOP_IP="192.000.0.000" # SET YOUR LAPTOP IP
8 |
9 | # Reset UFW to default settings
10 | sudo ufw reset
11 |
12 | # Deny all other incoming traffic
13 | sudo ufw default deny incoming
14 | sudo ufw default allow outgoing
15 |
16 | # Allow SSH
17 | sudo ufw allow ssh
18 |
19 | # Allow internal network traffic (assuming your private network range is 10.124.0.0/24)
20 | sudo ufw allow from 10.124.0.0/24
21 |
22 | # Allow Redis port (assuming Redis is exposed to internal network only)
23 | sudo ufw allow from 10.124.0.0/24 to any port 6379
24 |
25 | # Allow AsyncMon port for internal network
26 | sudo ufw allow from 10.124.0.0/24 to any port 8080
27 |
28 | # Allow AsyncMon port for your laptop IP
29 | sudo ufw allow from $YOUR_LAPTOP_IP to any port 8080
30 |
31 | # Enable UFW
32 | sudo ufw enable
33 |
34 | # Show UFW status
35 | sudo ufw status verbose
36 |
--------------------------------------------------------------------------------
/config/firewalls/web-app.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This firewall script allows access to the following ports:
3 | # - 443: HTTPS
4 | # - 22: SSH
5 |
6 | # Enable UFW and set default rules
7 | sudo ufw default deny incoming
8 | sudo ufw default allow outgoing
9 | sudo ufw enable
10 |
11 | # Allow SSH
12 | sudo ufw allow ssh
13 |
14 | # Fetch Cloudflare IPs and allow them for HTTPS
15 | echo "Fetching Cloudflare IPs..."
16 | curl -s https://www.cloudflare.com/ips-v4/ -o cloudflare-ips-v4.txt
17 |
18 | echo "Configuring UFW to allow Cloudflare IPs for HTTPS..."
19 | while IFS= read -r ip; do
20 | sudo ufw allow from $ip to any port 443
21 | done < cloudflare-ips-v4.txt
22 |
23 | # Clean up
24 | rm cloudflare-ips-v4.txt
25 |
26 | # Enable UFW
27 | sudo ufw enable
28 |
29 | echo "UFW configuration completed successfully."
30 |
31 | ###########################
32 | # Fail-to-ban
33 | ###########################
34 | # Setup fail to ban
35 | apt update && apt upgrade
36 | apt install fail2ban -y
37 | # Verify it's installed
38 | fail2ban-client -h
--------------------------------------------------------------------------------
/config/firewalls/worker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This firewall script allows access to the following ports:
3 | # - 22: SSH
4 |
5 | # Reset UFW to default settings
6 | sudo ufw reset
7 |
8 | # Deny all other incoming traffic
9 | sudo ufw default deny incoming
10 | sudo ufw default allow outgoing
11 |
12 | # Allow SSH
13 | sudo ufw allow ssh
14 |
15 | # Allow internal network traffic (assuming your private network range is 10.124.0.0/24)
16 | sudo ufw allow from 10.124.0.0/24
17 |
18 | # Enable UFW
19 | sudo ufw enable
20 |
21 | # Show UFW status
22 | sudo ufw status verbose
23 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | cache:
3 | image: "redis:alpine"
4 | container_name: goship_cache
5 | ports:
6 | - "127.0.0.1:${HOST_CACHE_PORT-6379}:6379"
7 |
8 | # db:
9 | # # PG 16 is currenly not supported by Ent ORM: https://github.com/ent/ent/issues/3750
10 | # image: ankane/pgvector:v0.5.1
11 | # container_name: goship_db
12 | # ports:
13 | # - "127.0.0.1:${HOST_DB_PORT-5432}:5432"
14 | # environment:
15 | # - POSTGRES_USER=admin
16 | # - POSTGRES_PASSWORD=admin
17 | # - POSTGRES_DB=goship_db
18 |
19 | mailpit:
20 | image: axllent/mailpit
21 | container_name: goship_mailpit
22 | restart: always
23 | volumes:
24 | - ./data:/data
25 | ports:
26 | - "${HOST_MAILPIT_HTTP_PORT-8025}:8025"
27 | - "${HOST_MAILPIT_SMTP_PORT-1025}:1025"
28 | environment:
29 | MP_MAX_MESSAGES: 5000
30 | MP_DATA_FILE: /data/mailpit.db
31 | MP_SMTP_AUTH_ACCEPT_ANY: 1
32 | MP_SMTP_AUTH_ALLOW_INSECURE: 1
33 |
34 |
--------------------------------------------------------------------------------
/e2e_tests/config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | development: {
3 | WEBSITE_URL: "http://localhost:8002",
4 | EMAIL_CAPTURE_PORTAL_URL: "http://localhost:8026",
5 | },
6 | production: {
7 | WEBSITE_URL: "https://yourproductionurl.com",
8 | },
9 | // other environments...
10 | };
11 |
12 | // Default configuration
13 | const defaultConfig = {
14 | WEBSITE_URL: "http://localhost:8002",
15 | };
16 |
17 | module.exports = { config, defaultConfig };
18 |
--------------------------------------------------------------------------------
/e2e_tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "e2e_tests",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "test:dev": "NODE_ENV=development playwright test",
9 | "test:prod": "NODE_ENV=production playwright test"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "@playwright/test": "^1.40.1",
16 | "playwright": "^1.40.1"
17 | },
18 | "devDependencies": {
19 | "@playwright/test": "^1.44.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/e2e_tests/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { PlaywrightTestConfig } from "@playwright/test";
2 |
3 | const config: PlaywrightTestConfig = {
4 | projects: [
5 | {
6 | name: "Chrome",
7 | use: { browserName: "chromium" },
8 | },
9 | ],
10 | workers: 1, // Adjust the number of workers as needed based on your CI environment capabilities
11 | retries: 2,
12 | };
13 | export default config;
14 |
--------------------------------------------------------------------------------
/ent/emojis/emojis.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package emojis
4 |
5 | import (
6 | "entgo.io/ent/dialect/sql"
7 | )
8 |
9 | const (
10 | // Label holds the string label denoting the emojis type in the database.
11 | Label = "emojis"
12 | // FieldID holds the string denoting the id field in the database.
13 | FieldID = "id"
14 | // FieldUnifiedCode holds the string denoting the unified_code field in the database.
15 | FieldUnifiedCode = "unified_code"
16 | // FieldShortcode holds the string denoting the shortcode field in the database.
17 | FieldShortcode = "shortcode"
18 | // Table holds the table name of the emojis in the database.
19 | Table = "emojis"
20 | )
21 |
22 | // Columns holds all SQL columns for emojis fields.
23 | var Columns = []string{
24 | FieldID,
25 | FieldUnifiedCode,
26 | FieldShortcode,
27 | }
28 |
29 | // ValidColumn reports if the column name is valid (part of the table columns).
30 | func ValidColumn(column string) bool {
31 | for i := range Columns {
32 | if column == Columns[i] {
33 | return true
34 | }
35 | }
36 | return false
37 | }
38 |
39 | var (
40 | // UnifiedCodeValidator is a validator for the "unified_code" field. It is called by the builders before save.
41 | UnifiedCodeValidator func(string) error
42 | // ShortcodeValidator is a validator for the "shortcode" field. It is called by the builders before save.
43 | ShortcodeValidator func(string) error
44 | )
45 |
46 | // OrderOption defines the ordering options for the Emojis queries.
47 | type OrderOption func(*sql.Selector)
48 |
49 | // ByID orders the results by the id field.
50 | func ByID(opts ...sql.OrderTermOption) OrderOption {
51 | return sql.OrderByField(FieldID, opts...).ToFunc()
52 | }
53 |
54 | // ByUnifiedCode orders the results by the unified_code field.
55 | func ByUnifiedCode(opts ...sql.OrderTermOption) OrderOption {
56 | return sql.OrderByField(FieldUnifiedCode, opts...).ToFunc()
57 | }
58 |
59 | // ByShortcode orders the results by the shortcode field.
60 | func ByShortcode(opts ...sql.OrderTermOption) OrderOption {
61 | return sql.OrderByField(FieldShortcode, opts...).ToFunc()
62 | }
63 |
--------------------------------------------------------------------------------
/ent/enttest/enttest.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package enttest
4 |
5 | import (
6 | "context"
7 |
8 | "github.com/mikestefanello/pagoda/ent"
9 | // required by schema hooks.
10 | _ "github.com/mikestefanello/pagoda/ent/runtime"
11 |
12 | "entgo.io/ent/dialect/sql/schema"
13 | "github.com/mikestefanello/pagoda/ent/migrate"
14 | )
15 |
16 | type (
17 | // TestingT is the interface that is shared between
18 | // testing.T and testing.B and used by enttest.
19 | TestingT interface {
20 | FailNow()
21 | Error(...any)
22 | }
23 |
24 | // Option configures client creation.
25 | Option func(*options)
26 |
27 | options struct {
28 | opts []ent.Option
29 | migrateOpts []schema.MigrateOption
30 | }
31 | )
32 |
33 | // WithOptions forwards options to client creation.
34 | func WithOptions(opts ...ent.Option) Option {
35 | return func(o *options) {
36 | o.opts = append(o.opts, opts...)
37 | }
38 | }
39 |
40 | // WithMigrateOptions forwards options to auto migration.
41 | func WithMigrateOptions(opts ...schema.MigrateOption) Option {
42 | return func(o *options) {
43 | o.migrateOpts = append(o.migrateOpts, opts...)
44 | }
45 | }
46 |
47 | func newOptions(opts []Option) *options {
48 | o := &options{}
49 | for _, opt := range opts {
50 | opt(o)
51 | }
52 | return o
53 | }
54 |
55 | // Open calls ent.Open and auto-run migration.
56 | func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Client {
57 | o := newOptions(opts)
58 | c, err := ent.Open(driverName, dataSourceName, o.opts...)
59 | if err != nil {
60 | t.Error(err)
61 | t.FailNow()
62 | }
63 | migrateSchema(t, c, o)
64 | return c
65 | }
66 |
67 | // NewClient calls ent.NewClient and auto-run migration.
68 | func NewClient(t TestingT, opts ...Option) *ent.Client {
69 | o := newOptions(opts)
70 | c := ent.NewClient(o.opts...)
71 | migrateSchema(t, c, o)
72 | return c
73 | }
74 | func migrateSchema(t TestingT, c *ent.Client, o *options) {
75 | tables, err := schema.CopyTables(migrate.Tables)
76 | if err != nil {
77 | t.Error(err)
78 | t.FailNow()
79 | }
80 | if err := migrate.Create(context.Background(), c.Schema, tables, o.migrateOpts...); err != nil {
81 | t.Error(err)
82 | t.FailNow()
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/ent/generate.go:
--------------------------------------------------------------------------------
1 | package ent
2 |
3 | //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
4 |
--------------------------------------------------------------------------------
/ent/hook/custom_hook.go:
--------------------------------------------------------------------------------
1 | package hook
2 |
3 | import (
4 | "context"
5 | "log"
6 | "time"
7 |
8 | "github.com/mikestefanello/pagoda/ent"
9 | )
10 |
11 | // EnsureUTCHook creates a hook that ensures specified time fields are in UTC.
12 | func EnsureUTCHook(timeFields ...string) ent.Hook {
13 | return func(next ent.Mutator) ent.Mutator {
14 | return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
15 | // Iterate over provided time field names and adjust them to UTC
16 | for _, fieldName := range timeFields {
17 | adjustTimeFieldToUTC(m, fieldName)
18 | }
19 | return next.Mutate(ctx, m)
20 | })
21 | }
22 | }
23 |
24 | // adjustTimeFieldToUTC checks if a time field is set in the mutation and adjusts it to UTC.
25 | func adjustTimeFieldToUTC(m ent.Mutation, fieldName string) {
26 | if value, exists := m.Field(fieldName); exists {
27 | if t, ok := value.(time.Time); ok {
28 | // Ensure the time is in UTC before setting it back
29 | m.SetField(fieldName, t.UTC())
30 | } else {
31 | log.Printf("Field %s is not a time.Time type", fieldName)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ent/migrate/migrations/20240907154749.sql:
--------------------------------------------------------------------------------
1 | -- Drop "committed_relationship_requests" table
2 | DROP TABLE "committed_relationship_requests";
3 |
--------------------------------------------------------------------------------
/ent/migrate/migrations/20240907155651.sql:
--------------------------------------------------------------------------------
1 | -- Drop "seen_answer_events" table
2 | DROP TABLE "seen_answer_events";
3 | -- Drop "answers" table
4 | DROP TABLE "answers";
5 | -- Drop "seen_private_message_events" table
6 | DROP TABLE "seen_private_message_events";
7 | -- Drop "private_messages" table
8 | DROP TABLE "private_messages";
9 | -- Drop "question_seen_events" table
10 | DROP TABLE "question_seen_events";
11 | -- Drop "questions" table
12 | DROP TABLE "questions";
13 |
--------------------------------------------------------------------------------
/ent/migrate/migrations/20240907160557.sql:
--------------------------------------------------------------------------------
1 | -- Modify "profiles" table
2 | ALTER TABLE "profiles" DROP COLUMN "min_interested_age", DROP COLUMN "max_interested_age", DROP COLUMN "latitude", DROP COLUMN "longitude", DROP COLUMN "radius", DROP COLUMN "num_matches";
3 | -- Drop "profile_disliked_profiles" table
4 | DROP TABLE "profile_disliked_profiles";
5 | -- Drop "profile_liked_profiles" table
6 | DROP TABLE "profile_liked_profiles";
7 | -- Drop "profile_matches" table
8 | DROP TABLE "profile_matches";
9 |
--------------------------------------------------------------------------------
/ent/migrate/migrations/20240907214903.sql:
--------------------------------------------------------------------------------
1 | -- Drop index "email_subscription_types_name_key" from table: "email_subscription_types"
2 | DROP INDEX "email_subscription_types_name_key";
3 |
--------------------------------------------------------------------------------
/ent/migrate/migrations/atlas.sum:
--------------------------------------------------------------------------------
1 | h1:b113FOeVMSmG+t7MWRsD6C1J4DqLkSebl5EHt7IS4Jo=
2 | 20240903200944.sql h1:ZyOhdwZuhO86mUHr8QDpkbvyNtXd0rh7lYr75Zt/C00=
3 | 20240907154749.sql h1:N0whZxy+XYfY6Y3aanlxAXttFnN2gXxOvT8Q9q/r5TU=
4 | 20240907155651.sql h1:mmpsRPJjXkScye1TCqsrBoZ9nqvZSud5F26oVsJto+U=
5 | 20240907160557.sql h1:MgvuM9ru8vh2+CgsHBqu4PIGUt1uDilrYW51b5a/txA=
6 | 20240907214903.sql h1:mvzqbHXtNJarHJUBUDW9RvQraVJVXHbX5WhbWAlSh/0=
7 |
--------------------------------------------------------------------------------
/ent/predicate/predicate.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package predicate
4 |
5 | import (
6 | "entgo.io/ent/dialect/sql"
7 | )
8 |
9 | // EmailSubscription is the predicate function for emailsubscription builders.
10 | type EmailSubscription func(*sql.Selector)
11 |
12 | // EmailSubscriptionType is the predicate function for emailsubscriptiontype builders.
13 | type EmailSubscriptionType func(*sql.Selector)
14 |
15 | // Emojis is the predicate function for emojis builders.
16 | type Emojis func(*sql.Selector)
17 |
18 | // FCMSubscriptions is the predicate function for fcmsubscriptions builders.
19 | type FCMSubscriptions func(*sql.Selector)
20 |
21 | // FileStorage is the predicate function for filestorage builders.
22 | type FileStorage func(*sql.Selector)
23 |
24 | // Image is the predicate function for image builders.
25 | type Image func(*sql.Selector)
26 |
27 | // ImageSize is the predicate function for imagesize builders.
28 | type ImageSize func(*sql.Selector)
29 |
30 | // Invitation is the predicate function for invitation builders.
31 | type Invitation func(*sql.Selector)
32 |
33 | // LastSeenOnline is the predicate function for lastseenonline builders.
34 | type LastSeenOnline func(*sql.Selector)
35 |
36 | // MonthlySubscription is the predicate function for monthlysubscription builders.
37 | type MonthlySubscription func(*sql.Selector)
38 |
39 | // Notification is the predicate function for notification builders.
40 | type Notification func(*sql.Selector)
41 |
42 | // NotificationPermission is the predicate function for notificationpermission builders.
43 | type NotificationPermission func(*sql.Selector)
44 |
45 | // NotificationTime is the predicate function for notificationtime builders.
46 | type NotificationTime func(*sql.Selector)
47 |
48 | // PasswordToken is the predicate function for passwordtoken builders.
49 | type PasswordToken func(*sql.Selector)
50 |
51 | // PhoneVerificationCode is the predicate function for phoneverificationcode builders.
52 | type PhoneVerificationCode func(*sql.Selector)
53 |
54 | // Profile is the predicate function for profile builders.
55 | type Profile func(*sql.Selector)
56 |
57 | // PwaPushSubscription is the predicate function for pwapushsubscription builders.
58 | type PwaPushSubscription func(*sql.Selector)
59 |
60 | // SentEmail is the predicate function for sentemail builders.
61 | type SentEmail func(*sql.Selector)
62 |
63 | // User is the predicate function for user builders.
64 | type User func(*sql.Selector)
65 |
--------------------------------------------------------------------------------
/ent/runtime.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package ent
4 |
5 | // The schema-stitching logic is generated in github.com/mikestefanello/pagoda/ent/runtime/runtime.go
6 |
--------------------------------------------------------------------------------
/ent/schema/emailsubscription.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | )
8 |
9 | // EmailSubscription holds the schema definition for the EmailSubscription entity.
10 | type EmailSubscription struct {
11 | ent.Schema
12 | }
13 |
14 | func (EmailSubscription) Mixin() []ent.Mixin {
15 | return []ent.Mixin{
16 | TimeMixin{},
17 | }
18 | }
19 |
20 | // Fields of the EmailSubscription.
21 | func (EmailSubscription) Fields() []ent.Field {
22 | return []ent.Field{
23 | field.String("email").
24 | NotEmpty().
25 | Unique(),
26 | field.Bool("verified").
27 | Default(false),
28 | field.String("confirmation_code").
29 | NotEmpty().
30 | Unique(),
31 | field.Float("latitude").
32 | Optional().
33 | Comment("The latitude of the subscriber's location."),
34 | field.Float("longitude").
35 | Optional().
36 | Comment("The longitude of the subscriber's location."),
37 | }
38 | }
39 |
40 | // Edges of the EmailSubscription.
41 | func (EmailSubscription) Edges() []ent.Edge {
42 | return []ent.Edge{
43 | edge.To("subscriptions", EmailSubscriptionType.Type).
44 | Comment("Subscriptions that this email is subscribed to"),
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ent/schema/emailsubscriptiontype.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "github.com/mikestefanello/pagoda/pkg/domain"
8 | )
9 |
10 | // TODO: rename to EmailSubscriptionList
11 | // EmailSubscriptionType holds the schema definition for the EmailSubscriptionType entity.
12 | type EmailSubscriptionType struct {
13 | ent.Schema
14 | }
15 |
16 | func (EmailSubscriptionType) Mixin() []ent.Mixin {
17 | return []ent.Mixin{
18 | TimeMixin{},
19 | }
20 | }
21 |
22 | // Fields of the EmailSubscriptionType.
23 | func (EmailSubscriptionType) Fields() []ent.Field {
24 | return []ent.Field{
25 | field.Enum("name").
26 | Values(domain.EmailSubscriptionLists.Values()...),
27 | field.Bool("active").
28 | Default(true),
29 | }
30 | }
31 |
32 | // Edges of the EmailSubscriptionType.
33 | func (EmailSubscriptionType) Edges() []ent.Edge {
34 | return []ent.Edge{
35 | edge.From("subscriber", EmailSubscription.Type).
36 | Ref("subscriptions").
37 | Comment("Subscriber subscribed to this subscription type."),
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ent/schema/emojis.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/field"
6 | )
7 |
8 | // Emojis holds the schema definition for the Emojis entity.
9 | type Emojis struct {
10 | ent.Schema
11 | }
12 |
13 | // Fields of the Emojis.
14 | func (Emojis) Fields() []ent.Field {
15 | return []ent.Field{
16 | field.String("unified_code").NotEmpty(),
17 | field.String("shortcode").NotEmpty(),
18 | }
19 | }
20 |
21 | // Edges of the Emojis.
22 | func (Emojis) Edges() []ent.Edge {
23 | return []ent.Edge{
24 | // edge.To("answer_reactions", AnswerReactions.Type),
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ent/schema/fcmsubscriptions.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "entgo.io/ent/schema/index"
8 | )
9 |
10 | // FCMSubscriptions holds the schema definition for the FCMSubscriptions entity.
11 | type FCMSubscriptions struct {
12 | ent.Schema
13 | }
14 |
15 | func (FCMSubscriptions) Mixin() []ent.Mixin {
16 | return []ent.Mixin{
17 | TimeMixin{},
18 | }
19 | }
20 |
21 | // Fields of the FCMSubscriptions.
22 | func (FCMSubscriptions) Fields() []ent.Field {
23 | return []ent.Field{
24 | field.String("token").NotEmpty(),
25 | field.Int("profile_id"),
26 | }
27 | }
28 |
29 | // Edges of the FCMSubscriptions.
30 | func (FCMSubscriptions) Edges() []ent.Edge {
31 | return []ent.Edge{
32 | edge.From("profile", Profile.Type).
33 | Ref("fcm_push_subscriptions").
34 | Field("profile_id").
35 | Required().
36 | Unique(),
37 | }
38 | }
39 |
40 | // Indexes of the FCMSubscriptions.
41 | func (FCMSubscriptions) Indexes() []ent.Index {
42 | return []ent.Index{
43 | index.Fields("token", "profile_id").Unique(),
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ent/schema/filestorage.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/field"
6 | "entgo.io/ent/schema/index"
7 | )
8 |
9 | // FileStorage holds the schema definition for the FileStorage entity.
10 | type FileStorage struct {
11 | ent.Schema
12 | }
13 |
14 | func (FileStorage) Mixin() []ent.Mixin {
15 | return []ent.Mixin{
16 | TimeMixin{},
17 | }
18 | }
19 |
20 | // Fields of the FileStorage.
21 | func (FileStorage) Fields() []ent.Field {
22 | return []ent.Field{
23 | field.String("bucket_name").NotEmpty(),
24 | field.String("object_key").NotEmpty(),
25 | field.String("original_file_name").Optional(),
26 | field.Int64("file_size").Optional(),
27 | field.String("content_type").Optional(),
28 | field.String("file_hash").Optional(),
29 | }
30 | }
31 |
32 | // Edges of the FileStorage.
33 | func (FileStorage) Edges() []ent.Edge {
34 | return nil
35 | }
36 |
37 | // Indexes of the FileStorage.
38 | func (FileStorage) Indexes() []ent.Index {
39 | return []ent.Index{
40 | index.Fields("bucket_name", "object_key").Unique(),
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ent/schema/image.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/dialect/entsql"
6 | "entgo.io/ent/schema/edge"
7 | "entgo.io/ent/schema/field"
8 | "github.com/mikestefanello/pagoda/pkg/domain"
9 | )
10 |
11 | func (Image) Mixin() []ent.Mixin {
12 | return []ent.Mixin{
13 | TimeMixin{},
14 | }
15 | }
16 |
17 | // Image holds the schema definition for the Image entity.
18 | type Image struct {
19 | ent.Schema
20 | }
21 |
22 | // Fields of the Image.
23 | func (Image) Fields() []ent.Field {
24 | return []ent.Field{
25 | field.Enum("type").
26 | Values(domain.ImageCategories.Values()...),
27 | }
28 | }
29 |
30 | // Edges of the Image.
31 | func (Image) Edges() []ent.Edge {
32 | return []ent.Edge{
33 | edge.To("sizes", ImageSize.Type).
34 | Annotations(entsql.OnDelete(entsql.Cascade)),
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ent/schema/imagesize.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/dialect/entsql"
6 | "entgo.io/ent/schema/edge"
7 | "entgo.io/ent/schema/field"
8 | "github.com/mikestefanello/pagoda/pkg/domain"
9 | )
10 |
11 | // ImageSize holds the schema definition for the ImageSize entity.
12 | type ImageSize struct {
13 | ent.Schema
14 | }
15 |
16 | func (ImageSize) Mixin() []ent.Mixin {
17 | return []ent.Mixin{
18 | TimeMixin{},
19 | }
20 | }
21 |
22 | // Fields of the ImageSize.
23 | func (ImageSize) Fields() []ent.Field {
24 | return []ent.Field{
25 | field.Enum("size").
26 | Values(domain.ImageSizes.Values()...).
27 | Comment("The size of this image instance"),
28 | field.Int("width").Positive(),
29 | field.Int("height").Positive(),
30 | }
31 | }
32 |
33 | // Edges of the ImageSize.
34 | func (ImageSize) Edges() []ent.Edge {
35 | return []ent.Edge{
36 | edge.To("file", FileStorage.Type).
37 | Unique().
38 | Required().
39 | Annotations(entsql.OnDelete(entsql.Cascade)),
40 | // We add the "Required" method to the builder
41 | // to make this edge required on entity creation.
42 | // i.e. Card cannot be created without its owner.
43 | edge.From("image", Image.Type).
44 | Ref("sizes").
45 | Unique(),
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ent/schema/invitation.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | )
8 |
9 | // Invitation holds the schema definition for the Invitation entity.
10 | type Invitation struct {
11 | ent.Schema
12 | }
13 |
14 | func (Invitation) Mixin() []ent.Mixin {
15 | return []ent.Mixin{
16 | TimeMixin{},
17 | }
18 | }
19 |
20 | // Fields of the Invitation.
21 | func (Invitation) Fields() []ent.Field {
22 | return []ent.Field{
23 | field.String("invitee_name").
24 | NotEmpty(),
25 | field.String("confirmation_code").
26 | NotEmpty().
27 | Unique(),
28 | }
29 | }
30 |
31 | // Edges of the Invitation.
32 | func (Invitation) Edges() []ent.Edge {
33 | return []ent.Edge{
34 | edge.From("inviter", Profile.Type).
35 | Ref("invitations").
36 | Unique().
37 | Required().
38 | Comment("The profile who created the invitation."),
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ent/schema/lastseenonline.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent"
7 | "entgo.io/ent/schema/edge"
8 | "entgo.io/ent/schema/field"
9 | )
10 |
11 | // LastSeenOnline holds the schema definition for the LastSeenOnline entity.
12 | type LastSeenOnline struct {
13 | ent.Schema
14 | }
15 |
16 | // Fields of the LastSeenOnline.
17 | func (LastSeenOnline) Fields() []ent.Field {
18 | return []ent.Field{
19 | field.Time("seen_at").
20 | Immutable().
21 | Default(time.Now),
22 | }
23 | }
24 |
25 | // Edges of the LastSeenOnline.
26 | func (LastSeenOnline) Edges() []ent.Edge {
27 | return []ent.Edge{
28 | edge.From("user", User.Type).
29 | Ref("last_seen_at").
30 | Unique().
31 | Required(),
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ent/schema/mixin.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent"
7 | "entgo.io/ent/schema/field"
8 | "entgo.io/ent/schema/mixin"
9 | )
10 |
11 | // -------------------------------------------------
12 | // Mixin definition
13 |
14 | // TimeMixin implements the ent.Mixin for sharing
15 | // time fields with package schemas.
16 | type TimeMixin struct {
17 | // We embed the `mixin.Schema` to avoid
18 | // implementing the rest of the methods.
19 | mixin.Schema
20 | }
21 |
22 | func (TimeMixin) Fields() []ent.Field {
23 | return []ent.Field{
24 | field.Time("created_at").
25 | Immutable().
26 | Default(time.Now),
27 | field.Time("updated_at").
28 | Default(time.Now).
29 | UpdateDefault(time.Now),
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/ent/schema/monthlysubscription.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/dialect/entsql"
6 | "entgo.io/ent/schema/edge"
7 | "entgo.io/ent/schema/field"
8 | "entgo.io/ent/schema/index"
9 | "github.com/mikestefanello/pagoda/pkg/domain"
10 | )
11 |
12 | // MonthlySubscription holds the schema definition for the MonthlySubscription entity.
13 | type MonthlySubscription struct {
14 | ent.Schema
15 | }
16 |
17 | func (MonthlySubscription) Mixin() []ent.Mixin {
18 | return []ent.Mixin{
19 | TimeMixin{},
20 | }
21 | }
22 |
23 | // Fields of the MonthlySubscription.
24 | func (MonthlySubscription) Fields() []ent.Field {
25 | return []ent.Field{
26 | field.Enum("product").
27 | Default(domain.ProductTypeFree.Value).
28 | Values(domain.ProductTypes.Values()...),
29 | field.Bool("is_active").
30 | Default(false).
31 | Comment("Whether this subscription is active or not."),
32 | field.Bool("paid").
33 | Default(false).
34 | Comment("Whether this subscription was paid or not."),
35 | field.Bool("is_trial").
36 | Default(true).
37 | Comment("Whether this subscription is a trial or not."),
38 |
39 | field.Time("started_at").
40 | Optional().
41 | Nillable().
42 | Comment("When the subscription started being effective."),
43 | field.Time("expired_on").
44 | Optional().
45 | Nillable().
46 | Comment("If the subscription expires, when it does so."),
47 | field.Time("cancelled_at").
48 | Optional().
49 | Nillable().
50 | Comment("Cancelling is effective after current period ends."),
51 | field.Int("paying_profile_id"),
52 | }
53 | }
54 |
55 | // Edges of the MonthlySubscription.
56 | func (MonthlySubscription) Edges() []ent.Edge {
57 | return []ent.Edge{
58 | edge.To("benefactors", Profile.Type).
59 | Comment("Who is on this subscription.").
60 | Annotations(entsql.OnDelete(entsql.NoAction)),
61 | edge.To("payer", Profile.Type).
62 | Comment("Who is paying for this subscription").
63 | Unique().
64 | Field("paying_profile_id").
65 | Required().
66 | Annotations(entsql.OnDelete(entsql.NoAction)),
67 | }
68 | }
69 |
70 | // Indexes of the MonthlySubscription.
71 | func (MonthlySubscription) Indexes() []ent.Index {
72 | return []ent.Index{
73 | // Someone can only have a single active subscription at a time.
74 | index.Fields("paying_profile_id", "is_active").Unique(),
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/ent/schema/notification.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "github.com/mikestefanello/pagoda/ent/hook"
8 | "github.com/mikestefanello/pagoda/pkg/domain"
9 | )
10 |
11 | // Notification holds the schema definition for the Notification entity.
12 | type Notification struct {
13 | ent.Schema
14 | }
15 |
16 | func (Notification) Mixin() []ent.Mixin {
17 | return []ent.Mixin{
18 | TimeMixin{},
19 | }
20 | }
21 |
22 | // Fields of the Notification.
23 | func (Notification) Fields() []ent.Field {
24 | return []ent.Field{
25 | field.Enum("type").
26 | Values(domain.NotificationTypes.Values()...).
27 | Comment("Type of notification (e.g., message, update)"),
28 | field.String("title").
29 | Default(""). // TODO: had to set a default because this field was added after model creation and had data in it.
30 | Comment("Title the notification"),
31 | field.String("text").
32 | Comment("Main content of the notification"),
33 | field.String("link").
34 | Optional().
35 | Nillable().
36 | Comment("Optional URL for the resource related to the notification"),
37 | field.Bool("read").
38 | Comment("Indicates if the notification has been read").
39 | Default(false),
40 | field.Time("read_at").
41 | Comment("Time when the notification was read").
42 | Optional().
43 | Nillable(),
44 | field.Int("profile_id_who_caused_notification").
45 | Optional().
46 | Nillable(),
47 | field.Int("resource_id_tied_to_notif").
48 | Optional().
49 | Nillable(),
50 | field.Bool("read_in_notifications_center").
51 | Optional().
52 | Nillable(),
53 | }
54 | }
55 |
56 | // Edges of the Notification.
57 | func (Notification) Edges() []ent.Edge {
58 | return []ent.Edge{
59 | edge.From("profile", Profile.Type).
60 | Ref("notifications").
61 | Unique(),
62 | }
63 | }
64 |
65 | func (Notification) Hooks() []ent.Hook {
66 | return []ent.Hook{
67 | hook.EnsureUTCHook(
68 | "read_at",
69 | ),
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/ent/schema/notificationpermission.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "entgo.io/ent/schema/index"
8 | "github.com/mikestefanello/pagoda/pkg/domain"
9 | )
10 |
11 | // NotificationPermission holds the schema definition for the NotificationPermission entity.
12 | type NotificationPermission struct {
13 | ent.Schema
14 | }
15 |
16 | func (NotificationPermission) Mixin() []ent.Mixin {
17 | return []ent.Mixin{
18 | TimeMixin{},
19 | }
20 | }
21 |
22 | // Fields of the NotificationPermission.
23 | func (NotificationPermission) Fields() []ent.Field {
24 | return []ent.Field{
25 | field.Enum("permission").
26 | Values(domain.NotificationPermissions.Values()...),
27 | field.Enum("platform").
28 | Values(domain.NotificationPlatforms.Values()...),
29 | field.Int("profile_id"),
30 | field.String("token").
31 | Comment("For permissions cancellable through out-of-app-platform, this is like an auth token"),
32 | }
33 | }
34 |
35 | // Edges of the NotificationPermission.
36 | func (NotificationPermission) Edges() []ent.Edge {
37 | return []ent.Edge{
38 | edge.From("profile", Profile.Type).
39 | Ref("notification_permissions").
40 | Field("profile_id").
41 | Required().
42 | Unique(),
43 | }
44 | }
45 |
46 | // Indexes of the Card.
47 | func (NotificationPermission) Indexes() []ent.Index {
48 | return []ent.Index{
49 | index.Fields("profile_id", "permission", "platform").Unique(),
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/ent/schema/notificationtime.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "entgo.io/ent/schema/index"
8 | "github.com/mikestefanello/pagoda/pkg/domain"
9 | )
10 |
11 | // NotificationTime holds the schema definition for the NotificationTime entity.
12 | type NotificationTime struct {
13 | ent.Schema
14 | }
15 |
16 | func (NotificationTime) Mixin() []ent.Mixin {
17 | return []ent.Mixin{
18 | TimeMixin{},
19 | }
20 | }
21 |
22 | // Fields of the NotificationTime.
23 | func (NotificationTime) Fields() []ent.Field {
24 | return []ent.Field{
25 | field.Enum("type").
26 | Values(domain.NotificationTypes.Values()...).
27 | Comment("Type of notification (e.g., message, update)"),
28 | field.Int("send_minute").
29 | Comment("Minutes since UTC midnight (0-1439) when the notification can be sent").
30 | Min(0).
31 | Max(1439),
32 | field.Int("profile_id").
33 | Comment("A user should only have 1 entry").
34 | Unique(),
35 | }
36 | }
37 |
38 | // Edges of the NotificationTime.
39 | func (NotificationTime) Edges() []ent.Edge {
40 | return []ent.Edge{
41 | edge.From("profile", Profile.Type).
42 | Ref("notification_times").
43 | Field("profile_id").
44 | Unique().
45 | Required(),
46 | }
47 | }
48 |
49 | // Indexes of the Profile.
50 | func (NotificationTime) Indexes() []ent.Index {
51 | return []ent.Index{
52 | // Someone can only have a single notification time per type
53 | index.Fields("profile_id", "type").Unique(),
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/ent/schema/passwordtoken.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent"
7 | "entgo.io/ent/schema/edge"
8 | "entgo.io/ent/schema/field"
9 | )
10 |
11 | // PasswordToken holds the schema definition for the PasswordToken entity.
12 | type PasswordToken struct {
13 | ent.Schema
14 | }
15 |
16 | // Fields of the PasswordToken.
17 | func (PasswordToken) Fields() []ent.Field {
18 | return []ent.Field{
19 | field.String("hash").
20 | Sensitive().
21 | NotEmpty(),
22 | field.Time("created_at").
23 | Default(time.Now),
24 | }
25 | }
26 |
27 | // Edges of the PasswordToken.
28 | func (PasswordToken) Edges() []ent.Edge {
29 | return []ent.Edge{
30 | edge.To("user", User.Type).
31 | Required().
32 | Unique(),
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ent/schema/phoneverificationcode.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "entgo.io/ent/schema/index"
8 | )
9 |
10 | // PhoneVerificationCode holds the schema definition for the PhoneVerificationCode entity.
11 | type PhoneVerificationCode struct {
12 | ent.Schema
13 | }
14 |
15 | func (PhoneVerificationCode) Mixin() []ent.Mixin {
16 | return []ent.Mixin{
17 | TimeMixin{},
18 | }
19 | }
20 |
21 | // Fields of the PhoneVerificationCode.
22 | func (PhoneVerificationCode) Fields() []ent.Field {
23 | return []ent.Field{
24 | field.String("code").
25 | Comment("The verification code"),
26 | field.Int("profile_id"),
27 | }
28 | }
29 |
30 | // Edges of the PhoneVerificationCode.
31 | func (PhoneVerificationCode) Edges() []ent.Edge {
32 | return []ent.Edge{
33 | edge.From("profile", Profile.Type).
34 | Ref("phone_verification_code").
35 | Field("profile_id").
36 | Unique().
37 | Required(),
38 | }
39 | }
40 |
41 | // Indexes of the Profile.
42 | func (PhoneVerificationCode) Indexes() []ent.Index {
43 | return []ent.Index{
44 | // Someone can only have a single active subscription at a time.
45 | index.Fields("code", "profile_id").Unique(),
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ent/schema/pwapushsubscription.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "entgo.io/ent/schema/index"
8 | )
9 |
10 | // PwaPushSubscription holds the schema definition for the PwaPushSubscription entity.
11 | type PwaPushSubscription struct {
12 | ent.Schema
13 | }
14 |
15 | func (PwaPushSubscription) Mixin() []ent.Mixin {
16 | return []ent.Mixin{
17 | TimeMixin{},
18 | }
19 | }
20 |
21 | // Fields of the PwaPushSubscription.
22 | func (PwaPushSubscription) Fields() []ent.Field {
23 | return []ent.Field{
24 | field.String("endpoint").NotEmpty(),
25 | field.String("p256dh").NotEmpty(),
26 | field.String("auth").NotEmpty(),
27 | field.Int("profile_id"),
28 | }
29 | }
30 |
31 | // Edges of the PwaPushSubscription.
32 | func (PwaPushSubscription) Edges() []ent.Edge {
33 | return []ent.Edge{
34 | edge.From("profile", Profile.Type).
35 | Ref("pwa_push_subscriptions").
36 | Field("profile_id").
37 | Required().
38 | Unique(),
39 | }
40 | }
41 |
42 | // Indexes of the Card.
43 | func (PwaPushSubscription) Indexes() []ent.Index {
44 | return []ent.Index{
45 | index.Fields("profile_id", "endpoint").Unique(),
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ent/schema/sentemail.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "github.com/mikestefanello/pagoda/pkg/domain"
8 | )
9 |
10 | // SentEmail holds the schema definition for the SentEmail entity.
11 | type SentEmail struct {
12 | ent.Schema
13 | }
14 |
15 | func (SentEmail) Mixin() []ent.Mixin {
16 | return []ent.Mixin{
17 | TimeMixin{},
18 | }
19 | }
20 |
21 | // Fields of the SentEmail.
22 | func (SentEmail) Fields() []ent.Field {
23 | return []ent.Field{
24 | field.Enum("type").
25 | Values(domain.NotificationPermissions.Values()...),
26 | }
27 | }
28 |
29 | // Edges of the SentEmail.
30 | func (SentEmail) Edges() []ent.Edge {
31 | return []ent.Edge{
32 | edge.From("profile", Profile.Type).
33 | Ref("sent_emails").
34 | Unique().
35 | Required(),
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ent/schema/user.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "context"
5 | "strings"
6 |
7 | ge "github.com/mikestefanello/pagoda/ent"
8 | "github.com/mikestefanello/pagoda/ent/hook"
9 |
10 | "entgo.io/ent"
11 | "entgo.io/ent/dialect/entsql"
12 | "entgo.io/ent/schema/edge"
13 | "entgo.io/ent/schema/field"
14 | )
15 |
16 | // User holds the schema definition for the User entity.
17 | type User struct {
18 | ent.Schema
19 | }
20 |
21 | func (User) Mixin() []ent.Mixin {
22 | return []ent.Mixin{
23 | TimeMixin{},
24 | }
25 | }
26 |
27 | // Fields of the User.
28 | func (User) Fields() []ent.Field {
29 | return []ent.Field{
30 | field.String("name").
31 | NotEmpty(),
32 | field.String("email").
33 | NotEmpty().
34 | Unique(),
35 | field.String("password").
36 | Sensitive().
37 | NotEmpty(),
38 | field.Bool("verified").
39 | Default(false),
40 | field.Time("last_online").
41 | Optional(),
42 | }
43 | }
44 |
45 | // Edges of the User.
46 | func (User) Edges() []ent.Edge {
47 | return []ent.Edge{
48 | edge.From("owner", PasswordToken.Type).
49 | Ref("user"),
50 |
51 | edge.To("profile", Profile.Type).
52 | Unique().
53 | Annotations(entsql.OnDelete(entsql.Cascade)),
54 | edge.To("last_seen_at", LastSeenOnline.Type).
55 | Annotations(entsql.OnDelete(entsql.Cascade)),
56 | }
57 | }
58 |
59 | // Hooks of the User.
60 | func (User) Hooks() []ent.Hook {
61 | return []ent.Hook{
62 | hook.On(
63 | func(next ent.Mutator) ent.Mutator {
64 | return hook.UserFunc(func(ctx context.Context, m *ge.UserMutation) (ent.Value, error) {
65 | if v, exists := m.Email(); exists {
66 | m.SetEmail(strings.ToLower(v))
67 | }
68 | return next.Mutate(ctx, m)
69 | })
70 | },
71 | // Limit the hook only for these operations.
72 | ent.OpCreate|ent.OpUpdate|ent.OpUpdateOne,
73 | ),
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Default to running the app if no arguments are given
4 | if [ "$#" -eq 0 ]; then
5 | exec /goship-web
6 | fi
7 |
8 | # Otherwise, run the specified executable
9 | case "$1" in
10 | web)
11 | exec /goship-web
12 | ;;
13 | worker)
14 | /goship-worker &
15 | # asynqmon --port=8080 --redis-addr=localhost:6379 &
16 | wait
17 | ;;
18 | seeder)
19 | exec /goship-seeder
20 | ;;
21 | *)
22 | echo "Unknown command: $1"
23 | exit 1
24 | ;;
25 | esac
26 |
27 | # docker run --rm --name asynqmon -e REDIS_URL="redis:164.92.66.136:6379" -p 8080:8080 hibiken/asynqmon
--------------------------------------------------------------------------------
/javascript/svelte/components/SvelteTodoComponent.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
Todos
26 |
27 | event.key === 'Enter' && addTodo()}
31 | placeholder="Add new todo"
32 | />
33 |
39 |
40 |
41 | {#each todos as todo}
42 |
43 |
44 |
toggleDone(todo.id)} />
45 |
46 | {todo.text}
47 |
48 |
49 |
55 |
56 | {/each}
57 |
58 |
59 |
60 |
65 |
--------------------------------------------------------------------------------
/javascript/svelte/components/ThemeToggle.svelte:
--------------------------------------------------------------------------------
1 |
32 |
33 |
45 |
--------------------------------------------------------------------------------
/javascript/svelte/components/notifications/IndividualNotificationPermission.svelte:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/javascript/svelte/components/notifications/PermissionButton.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
19 |
--------------------------------------------------------------------------------
/javascript/svelte/components/notifications/icons/EmailDisabledIcon.svelte:
--------------------------------------------------------------------------------
1 |
3 |
4 |
21 |
--------------------------------------------------------------------------------
/javascript/svelte/components/notifications/icons/EmailEnabledIcon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/javascript/svelte/components/notifications/icons/LoadingSpinner.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
25 |
--------------------------------------------------------------------------------
/javascript/svelte/components/notifications/icons/PushDisabledIcon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/javascript/svelte/components/notifications/icons/PushEnabledIcon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/javascript/svelte/components/notifications/icons/SmsDisabledIcon.svelte:
--------------------------------------------------------------------------------
1 |
3 |
4 |
21 |
--------------------------------------------------------------------------------
/javascript/svelte/components/notifications/icons/SmsEnabledIcon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/javascript/svelte/components/utils.js:
--------------------------------------------------------------------------------
1 | function showSuccessToast(message) {
2 | toast.success(message);
3 | }
4 |
--------------------------------------------------------------------------------
/javascript/vanilla/load_scripts_and_styles.js:
--------------------------------------------------------------------------------
1 | export function loadScriptsAndStyles(files) {
2 | // Promise array to track loading status
3 | let promises = [];
4 |
5 | // Helper function to create a promise for each file
6 | function createPromise(file) {
7 | return new Promise((resolve, reject) => {
8 | let element;
9 |
10 | if (file.endsWith(".js")) {
11 | // Create script element for JavaScript file
12 | element = document.createElement("script");
13 | element.src = file;
14 | element.onload = resolve;
15 | element.onerror = reject;
16 | document.head.appendChild(element);
17 | } else if (file.endsWith(".css")) {
18 | // Create link element for CSS file
19 | element = document.createElement("link");
20 | element.rel = "stylesheet";
21 | element.href = file;
22 | element.onload = resolve;
23 | element.onerror = reject;
24 | document.head.appendChild(element);
25 | } else {
26 | reject(new Error(`Unsupported file type: ${file}`));
27 | }
28 | });
29 | }
30 |
31 | // Create promises for each file and push them to the promises array
32 | files.forEach((file) => {
33 | promises.push(createPromise(file));
34 | });
35 |
36 | // Return a promise that resolves when all promises are resolved
37 | return Promise.all(promises);
38 | }
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "amie",
3 | "version": "1.0.0",
4 | "description": "A collection of tools to make web apps rapidly",
5 | "type": "module",
6 | "scripts": {
7 | "build": "node build.mjs",
8 | "watch": "watchexec --project-origin . -w javascript node build.mjs"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/leomorpho/pagoda.git"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/leomorpho/pagoda/issues"
19 | },
20 | "homepage": "https://github.com/leomorpho/pagoda#readme",
21 | "devDependencies": {
22 | "@faker-js/faker": "^8.4.1",
23 | "@types/node": "^20.12.7",
24 | "daisyui": "^4.6.2",
25 | "esbuild": "^0.20.0",
26 | "esbuild-svelte": "^0.8.0",
27 | "flowbite": "^2.3.0",
28 | "flowbite-svelte": "^0.44.24",
29 | "svelte": "^4.2.10",
30 | "svelte-preprocess": "^5.1.3",
31 | "tailwindcss": "^3.4.1",
32 | "typescript": "^5.4.2"
33 | },
34 | "dependencies": {
35 | "@deck.gl/mapbox": "^8.9.35",
36 | "@khmyznikov/pwa-install": "^0.3.7",
37 | "@tiptap/extension-floating-menu": "^2.3.0",
38 | "@tiptap/extension-placeholder": "^2.3.0",
39 | "@tiptap/starter-kit": "^2.3.0",
40 | "@use-gesture/vanilla": "^10.3.0",
41 | "@virgilsecurity/e3kit-browser": "^3.0.5",
42 | "clsx": "^2.1.0",
43 | "svelte-maplibre": "^0.8.2",
44 | "svelte-multiselect": "^10.2.0",
45 | "svelte-tel-input": "^3.3.9",
46 | "svelte-tiptap": "^1.1.2",
47 | "uppload": "^2.7.2",
48 | "wc-toast": "^1.3.1",
49 | "workbox-precaching": "^7.0.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/pgvector-image/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use the pgvector base image
2 | FROM ankane/pgvector:v0.5.1
3 |
4 | # Set environment variables for PostgreSQL
5 | ENV POSTGRES_USER=postgres
6 | ENV POSTGRES_DB=postgres
7 | ENV POSTGRES_HOST_AUTH_METHOD=trust
8 |
9 | # Add initialization script
10 | COPY init_pgvector.sh /docker-entrypoint-initdb.d/
11 |
12 | # Ensure the script has the correct permissions
13 | RUN chmod +x /docker-entrypoint-initdb.d/init_pgvector.sh
--------------------------------------------------------------------------------
/pgvector-image/init_pgvector.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Function to check if PostgreSQL is ready
5 | check_postgres() {
6 | pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
7 | }
8 |
9 | # Wait for PostgreSQL to be ready
10 | echo "Waiting for PostgreSQL to start..."
11 | until check_postgres; do
12 | echo "PostgreSQL is unavailable - sleeping"
13 | sleep 2
14 | done
15 |
16 | # Create the extension
17 | echo "PostgreSQL is up - creating extension"
18 | psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS vector;"
19 |
--------------------------------------------------------------------------------
/pkg/context/context.go:
--------------------------------------------------------------------------------
1 | package context // TODO: rename this package, it conflicts with the std lib
2 |
3 | import (
4 | "context"
5 | "errors"
6 | )
7 |
8 | const (
9 | // AuthenticatedUserKey is the key value used to store the authenticated user in context
10 | AuthenticatedUserKey = "auth_user"
11 | AuthenticatedUserProfilePicURL = "profile_pic_url"
12 | ProfileFullyOnboarded = "profile_fully_onboarded"
13 | ActiveProductPlan = "product_plan"
14 |
15 | // UserKey is the key value used to store a user in context
16 | UserKey = "user"
17 |
18 | // FormKey is the key value used to store a form in context
19 | FormKey = "form"
20 |
21 | // PasswordTokenKey is the key value used to store a password token in context
22 | PasswordTokenKey = "password_token"
23 |
24 | IsFromIOSApp = "is_from_ios_app"
25 | )
26 |
27 | // IsCanceledError determines if an error is due to a context cancelation
28 | func IsCanceledError(err error) bool {
29 | return errors.Is(err, context.Canceled)
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/context/context_test.go:
--------------------------------------------------------------------------------
1 | package context
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestIsCanceled(t *testing.T) {
13 | ctx, cancel := context.WithCancel(context.Background())
14 | assert.False(t, IsCanceledError(ctx.Err()))
15 | cancel()
16 | assert.True(t, IsCanceledError(ctx.Err()))
17 |
18 | ctx, cancel = context.WithTimeout(context.Background(), time.Microsecond*5)
19 | <-ctx.Done()
20 | cancel()
21 | assert.False(t, IsCanceledError(ctx.Err()))
22 |
23 | assert.False(t, IsCanceledError(errors.New("test error")))
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/controller/form_test.go:
--------------------------------------------------------------------------------
1 | package controller_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mikestefanello/pagoda/pkg/controller"
7 | "github.com/mikestefanello/pagoda/pkg/tests"
8 |
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestFormSubmission(t *testing.T) {
14 | type formTest struct {
15 | Name string `validate:"required"`
16 | Email string `validate:"required,email"`
17 | Submission controller.FormSubmission
18 | }
19 |
20 | ctx, _ := tests.NewContext(c.Web, "/")
21 | form := formTest{
22 | Name: "",
23 | Email: "a@a.com",
24 | }
25 | err := form.Submission.Process(ctx, form)
26 | assert.NoError(t, err)
27 |
28 | assert.True(t, form.Submission.HasErrors())
29 | assert.True(t, form.Submission.FieldHasErrors("Name"))
30 | assert.False(t, form.Submission.FieldHasErrors("Email"))
31 | require.Len(t, form.Submission.GetFieldErrors("Name"), 1)
32 | assert.Len(t, form.Submission.GetFieldErrors("Email"), 0)
33 | assert.Equal(t, "This field is required.", form.Submission.GetFieldErrors("Name")[0])
34 | assert.Equal(t, "is-danger", form.Submission.GetFieldStatusClass("Name"))
35 | assert.Equal(t, "is-success", form.Submission.GetFieldStatusClass("Email"))
36 | assert.False(t, form.Submission.IsDone())
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/controller/page_test.go:
--------------------------------------------------------------------------------
1 | package controller_test
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | echomw "github.com/labstack/echo/v4/middleware"
8 | "github.com/mikestefanello/pagoda/pkg/context"
9 | "github.com/mikestefanello/pagoda/pkg/controller"
10 | "github.com/mikestefanello/pagoda/pkg/repos/msg"
11 | "github.com/mikestefanello/pagoda/pkg/tests"
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/require"
14 | )
15 |
16 | func TestNewPage(t *testing.T) {
17 | ctx, _ := tests.NewContext(c.Web, "/")
18 | p := controller.NewPage(ctx)
19 | assert.Same(t, ctx, p.Context)
20 | assert.NotNil(t, p.ToURL)
21 | assert.Equal(t, "/", p.Path)
22 | assert.Equal(t, "/", p.URL)
23 | assert.Equal(t, http.StatusOK, p.StatusCode)
24 | assert.Equal(t, controller.NewPager(ctx, controller.DefaultItemsPerPage), p.Pager)
25 | assert.Empty(t, p.Headers)
26 | assert.True(t, p.IsHome)
27 | assert.False(t, p.IsAuth)
28 | assert.Empty(t, p.CSRF)
29 | assert.Empty(t, p.RequestID)
30 | assert.False(t, p.Cache.Enabled)
31 |
32 | ctx, _ = tests.NewContext(c.Web, "/abc?def=123")
33 | usr, err := tests.CreateRandomUser(c.ORM)
34 | require.NoError(t, err)
35 | ctx.Set(context.AuthenticatedUserKey, usr)
36 | ctx.Set(echomw.DefaultCSRFConfig.ContextKey, "csrf")
37 | p = controller.NewPage(ctx)
38 | assert.Equal(t, "/abc", p.Path)
39 | assert.Equal(t, "/abc?def=123", p.URL)
40 | assert.False(t, p.IsHome)
41 | assert.True(t, p.IsAuth)
42 | assert.Equal(t, usr, p.AuthUser)
43 | assert.Equal(t, "csrf", p.CSRF)
44 | }
45 |
46 | func TestPage_GetMessages(t *testing.T) {
47 | ctx, _ := tests.NewContext(c.Web, "/")
48 | tests.InitSession(ctx)
49 | p := controller.NewPage(ctx)
50 |
51 | // Set messages
52 | msgTests := make(map[msg.Type][]string)
53 | msgTests[msg.TypeWarning] = []string{
54 | "abc",
55 | "def",
56 | }
57 | msgTests[msg.TypeInfo] = []string{
58 | "123",
59 | "456",
60 | }
61 | for typ, values := range msgTests {
62 | for _, value := range values {
63 | msg.Set(ctx, typ, value)
64 | }
65 | }
66 |
67 | // Get the messages
68 | for typ, values := range msgTests {
69 | msgs := p.GetMessages(typ)
70 |
71 | for i, message := range msgs {
72 | assert.Equal(t, values[i], string(message))
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/controller/pager_test.go:
--------------------------------------------------------------------------------
1 | package controller_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/mikestefanello/pagoda/pkg/controller"
8 | "github.com/mikestefanello/pagoda/pkg/tests"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestNewPager(t *testing.T) {
14 | ctx, _ := tests.NewContext(c.Web, "/")
15 | pgr := controller.NewPager(ctx, 10)
16 | assert.Equal(t, 10, pgr.ItemsPerPage)
17 | assert.Equal(t, 1, pgr.Page)
18 | assert.Equal(t, 0, pgr.Items)
19 | assert.Equal(t, 0, pgr.Pages)
20 |
21 | ctx, _ = tests.NewContext(c.Web, fmt.Sprintf("/abc?%s=%d", controller.PageQueryKey, 2))
22 | pgr = controller.NewPager(ctx, 10)
23 | assert.Equal(t, 2, pgr.Page)
24 |
25 | ctx, _ = tests.NewContext(c.Web, fmt.Sprintf("/abc?%s=%d", controller.PageQueryKey, -2))
26 | pgr = controller.NewPager(ctx, 10)
27 | assert.Equal(t, 1, pgr.Page)
28 | }
29 |
30 | func TestPager_SetItems(t *testing.T) {
31 | ctx, _ := tests.NewContext(c.Web, "/")
32 | pgr := controller.NewPager(ctx, 20)
33 | pgr.SetItems(100)
34 | assert.Equal(t, 100, pgr.Items)
35 | assert.Equal(t, 5, pgr.Pages)
36 | }
37 |
38 | func TestPager_IsBeginning(t *testing.T) {
39 | ctx, _ := tests.NewContext(c.Web, "/")
40 | pgr := controller.NewPager(ctx, 20)
41 | pgr.Pages = 10
42 | assert.True(t, pgr.IsBeginning())
43 | pgr.Page = 2
44 | assert.False(t, pgr.IsBeginning())
45 | pgr.Page = 1
46 | assert.True(t, pgr.IsBeginning())
47 | }
48 |
49 | func TestPager_IsEnd(t *testing.T) {
50 | ctx, _ := tests.NewContext(c.Web, "/")
51 | pgr := controller.NewPager(ctx, 20)
52 | pgr.Pages = 10
53 | assert.False(t, pgr.IsEnd())
54 | pgr.Page = 10
55 | assert.True(t, pgr.IsEnd())
56 | pgr.Page = 1
57 | assert.False(t, pgr.IsEnd())
58 | }
59 |
60 | func TestPager_GetOffset(t *testing.T) {
61 | ctx, _ := tests.NewContext(c.Web, "/")
62 | pgr := controller.NewPager(ctx, 20)
63 | assert.Equal(t, 0, pgr.GetOffset())
64 | pgr.Page = 2
65 | assert.Equal(t, 20, pgr.GetOffset())
66 | pgr.Page = 3
67 | assert.Equal(t, 40, pgr.GetOffset())
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/domain/constants.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import "time"
4 |
5 | var DefaultBirthdate = time.Date(1898, time.January, 6, 0, 0, 0, 0, time.UTC)
6 |
7 | // ImageSizeEnumToSizeMap maps an image size name to its actual size
8 | var ImageSizeEnumToSizeMap = map[ImageSize]int{
9 | ImageSizeThumbnail: 150, // max 150 pixels wide or tall
10 | ImageSizePreview: 800, // max 800 pixels wide or tall
11 | ImageSizeFull: 1600, // max 1600 pixels wide or tall
12 | }
13 |
14 | const (
15 | DefaultBio = "Hello, there! 🌟"
16 | FreePlanNumAnswersPerDay = 1
17 | PermissionNotificationType = "permission"
18 | )
19 |
--------------------------------------------------------------------------------
/pkg/domain/maps.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | // Initialize the map of NotificationPermissionType to NotificationPermission
4 | var NotificationPermissionMap = map[NotificationPermissionType]NotificationPermission{
5 | NotificationPermissionDailyReminder: {
6 | Title: "Daily conversation",
7 | Subtitle: "A reminder to not miss today's question, sent at most once a day.",
8 | Permission: NotificationPermissionDailyReminder.Value,
9 | },
10 | NotificationPermissionNewFriendActivity: {
11 | Title: "Partner activity",
12 | Subtitle: "Answers you missed, sent at most once a day.",
13 | Permission: NotificationPermissionNewFriendActivity.Value,
14 | },
15 | }
16 |
17 | var NotificationCenterButtonText = map[NotificationType]string{
18 | NotificationTypeConnectionEngagedWithQuestion: "Answer",
19 | }
20 |
21 | // DeleteOnceReadNotificationTypesMap is a map of notification types th;oiSJDfiujladijrgoizdikrjgat can be deleted once seen.
22 | // Note that the boolean doesn't matter, this is just a lazy way of creating a set in Go.
23 | var DeleteOnceReadNotificationTypesMap = map[NotificationType]bool{
24 | NotificationTypeDailyConversationReminder: true,
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/funcmap/funcmap.go:
--------------------------------------------------------------------------------
1 | package funcmap
2 |
3 | import (
4 | "fmt"
5 | "html/template"
6 | "reflect"
7 | "strings"
8 |
9 | "github.com/mikestefanello/pagoda/config"
10 |
11 | "github.com/Masterminds/sprig"
12 | "github.com/labstack/gommon/random"
13 | )
14 |
15 | var (
16 | // CacheBuster stores a random string used as a cache buster for static files.
17 | CacheBuster = random.String(10)
18 | )
19 |
20 | // GetFuncMap provides a template function map
21 | func GetFuncMap() template.FuncMap {
22 | // See http://masterminds.github.io/sprig/ for available funcs
23 | funcMap := sprig.FuncMap()
24 |
25 | // Provide a list of custom functions
26 | // Expand this as you add more functions to this package
27 | // Avoid using a name already in use by sprig
28 | f := template.FuncMap{
29 | "hasField": HasField,
30 | "file": File,
31 | "link": Link,
32 | }
33 |
34 | for k, v := range f {
35 | funcMap[k] = v
36 | }
37 |
38 | return funcMap
39 | }
40 |
41 | // HasField checks if an interface contains a given field
42 | func HasField(v any, name string) bool {
43 | rv := reflect.ValueOf(v)
44 | if rv.Kind() == reflect.Ptr {
45 | rv = rv.Elem()
46 | }
47 | if rv.Kind() != reflect.Struct {
48 | return false
49 | }
50 | return rv.FieldByName(name).IsValid()
51 | }
52 |
53 | // File appends a cache buster to a given filepath so it can remain cached until the app is restarted
54 | func File(filepath string) string {
55 | return fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, filepath, CacheBuster)
56 | }
57 |
58 | // Link outputs HTML for a link element, providing the ability to dynamically set the active class
59 | func Link(url, text, currentPath string, classes ...string) template.HTML {
60 | if currentPath == url {
61 | classes = append(classes, "is-active")
62 | }
63 |
64 | html := fmt.Sprintf(`%s`, strings.Join(classes, " "), url, text)
65 | return template.HTML(html)
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/funcmap/funcmap_test.go:
--------------------------------------------------------------------------------
1 | package funcmap
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/mikestefanello/pagoda/config"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestHasField(t *testing.T) {
13 | type example struct {
14 | name string
15 | }
16 | var e example
17 | assert.True(t, HasField(e, "name"))
18 | assert.False(t, HasField(e, "abcd"))
19 | }
20 |
21 | func TestLink(t *testing.T) {
22 | link := string(Link("/abc", "Text", "/abc"))
23 | expected := `Text`
24 | assert.Equal(t, expected, link)
25 |
26 | link = string(Link("/abc", "Text", "/abc", "first", "second"))
27 | expected = `Text`
28 | assert.Equal(t, expected, link)
29 |
30 | link = string(Link("/abc", "Text", "/def"))
31 | expected = `Text`
32 | assert.Equal(t, expected, link)
33 | }
34 |
35 | func TestFile(t *testing.T) {
36 | file := File("test.png")
37 | expected := fmt.Sprintf("/%s/test.png?v=%s", config.StaticPrefix, CacheBuster)
38 | assert.Equal(t, expected, file)
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/htmx/htmx_test.go:
--------------------------------------------------------------------------------
1 | package htmx
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | "github.com/mikestefanello/pagoda/pkg/tests"
8 |
9 | "github.com/stretchr/testify/assert"
10 |
11 | "github.com/labstack/echo/v4"
12 | )
13 |
14 | func TestSetRequest(t *testing.T) {
15 | ctx, _ := tests.NewContext(echo.New(), "/")
16 | ctx.Request().Header.Set(HeaderRequest, "true")
17 | ctx.Request().Header.Set(HeaderBoosted, "true")
18 | ctx.Request().Header.Set(HeaderTrigger, "a")
19 | ctx.Request().Header.Set(HeaderTriggerName, "b")
20 | ctx.Request().Header.Set(HeaderTarget, "c")
21 | ctx.Request().Header.Set(HeaderPrompt, "d")
22 |
23 | r := GetRequest(ctx)
24 | assert.Equal(t, true, r.Enabled)
25 | assert.Equal(t, true, r.Boosted)
26 | assert.Equal(t, "a", r.Trigger)
27 | assert.Equal(t, "b", r.TriggerName)
28 | assert.Equal(t, "c", r.Target)
29 | assert.Equal(t, "d", r.Prompt)
30 | }
31 |
32 | func TestResponse_Apply(t *testing.T) {
33 | ctx, _ := tests.NewContext(echo.New(), "/")
34 | r := Response{
35 | Push: "a",
36 | Redirect: "b",
37 | Refresh: true,
38 | Trigger: "c",
39 | TriggerAfterSwap: "d",
40 | TriggerAfterSettle: "e",
41 | NoContent: true,
42 | }
43 | r.Apply(ctx)
44 |
45 | assert.Equal(t, "a", ctx.Response().Header().Get(HeaderPush))
46 | assert.Equal(t, "b", ctx.Response().Header().Get(HeaderRedirect))
47 | assert.Equal(t, "true", ctx.Response().Header().Get(HeaderRefresh))
48 | assert.Equal(t, "c", ctx.Response().Header().Get(HeaderTrigger))
49 | assert.Equal(t, "d", ctx.Response().Header().Get(HeaderTriggerAfterSwap))
50 | assert.Equal(t, "e", ctx.Response().Header().Get(HeaderTriggerAfterSettle))
51 | assert.Equal(t, http.StatusNoContent, ctx.Response().Status)
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/middleware/cache_test.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "testing"
7 | "time"
8 |
9 | "github.com/mikestefanello/pagoda/pkg/tests"
10 |
11 | "github.com/stretchr/testify/require"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func TestServeCachedPage(t *testing.T) {
17 | // Cache a page
18 | cp := CachedPage{
19 | URL: "/cache",
20 | HTML: []byte("html"),
21 | Headers: make(map[string]string),
22 | StatusCode: http.StatusCreated,
23 | }
24 | cp.Headers["a"] = "b"
25 | cp.Headers["c"] = "d"
26 |
27 | err := c.Cache.
28 | Set().
29 | Group(CachedPageGroup).
30 | Key(cp.URL).
31 | Data(cp).
32 | Save(context.Background())
33 | require.NoError(t, err)
34 |
35 | // Request the URL of the cached page
36 | ctx, rec := tests.NewContext(c.Web, cp.URL)
37 | err = tests.ExecuteMiddleware(ctx, ServeCachedPage(c.Cache))
38 | assert.NoError(t, err)
39 | assert.Equal(t, cp.StatusCode, ctx.Response().Status)
40 | assert.Equal(t, cp.Headers["a"], ctx.Response().Header().Get("a"))
41 | assert.Equal(t, cp.Headers["c"], ctx.Response().Header().Get("c"))
42 | assert.Equal(t, cp.HTML, rec.Body.Bytes())
43 |
44 | // Login and try again
45 | tests.InitSession(ctx)
46 | err = c.Auth.Login(ctx, usr.ID)
47 | require.NoError(t, err)
48 | _ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth, nil, nil))
49 | err = tests.ExecuteMiddleware(ctx, ServeCachedPage(c.Cache))
50 | assert.Nil(t, err)
51 | }
52 |
53 | func TestCacheControl(t *testing.T) {
54 | ctx, _ := tests.NewContext(c.Web, "/")
55 | _ = tests.ExecuteMiddleware(ctx, CacheControl(time.Second*5))
56 | assert.Equal(t, "public, max-age=5", ctx.Response().Header().Get("Cache-Control"))
57 | _ = tests.ExecuteMiddleware(ctx, CacheControl(0))
58 | assert.Equal(t, "no-cache, no-store", ctx.Response().Header().Get("Cache-Control"))
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/middleware/device.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/context"
5 |
6 | "github.com/labstack/echo/v4"
7 | )
8 |
9 | // LoadAuthenticatedUser loads the authenticated user, if one, and stores in context
10 | func SetDeviceTypeToServe() echo.MiddlewareFunc {
11 | return func(next echo.HandlerFunc) echo.HandlerFunc {
12 | return func(c echo.Context) error {
13 |
14 | // Check for `app-platform` cookie
15 | appPlatformCookie, err := c.Cookie("app-platform")
16 | var isiOSApp bool
17 | if err == nil && appPlatformCookie != nil {
18 | isiOSApp = appPlatformCookie.Value == "iOS App Store"
19 | }
20 | c.Set(context.IsFromIOSApp, isiOSApp)
21 |
22 | return next(c)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/middleware/entity.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strconv"
7 |
8 | "github.com/mikestefanello/pagoda/ent"
9 | "github.com/mikestefanello/pagoda/ent/user"
10 | "github.com/mikestefanello/pagoda/pkg/context"
11 |
12 | "github.com/labstack/echo/v4"
13 | )
14 |
15 | // LoadUser loads the user based on the ID provided as a path parameter
16 | func LoadUser(orm *ent.Client) echo.MiddlewareFunc {
17 | return func(next echo.HandlerFunc) echo.HandlerFunc {
18 | return func(c echo.Context) error {
19 | userID, err := strconv.Atoi(c.Param("user"))
20 | if err != nil {
21 | return echo.NewHTTPError(http.StatusNotFound)
22 | }
23 |
24 | u, err := orm.User.
25 | Query().
26 | Where(user.ID(userID)).
27 | Only(c.Request().Context())
28 |
29 | switch err.(type) {
30 | case nil:
31 | c.Set(context.UserKey, u)
32 | return next(c)
33 | case *ent.NotFoundError:
34 | return echo.NewHTTPError(http.StatusNotFound)
35 | default:
36 | return echo.NewHTTPError(
37 | http.StatusInternalServerError,
38 | fmt.Sprintf("error querying user: %v", err),
39 | )
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/middleware/entity_test.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/mikestefanello/pagoda/ent"
8 | "github.com/mikestefanello/pagoda/pkg/context"
9 | "github.com/mikestefanello/pagoda/pkg/tests"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestLoadUser(t *testing.T) {
16 | ctx, _ := tests.NewContext(c.Web, "/")
17 | ctx.SetParamNames("user")
18 | ctx.SetParamValues(fmt.Sprintf("%d", usr.ID))
19 | _ = tests.ExecuteMiddleware(ctx, LoadUser(c.ORM))
20 | ctxUsr, ok := ctx.Get(context.UserKey).(*ent.User)
21 | require.True(t, ok)
22 | assert.Equal(t, usr.ID, ctxUsr.ID)
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/middleware/lastseenonline.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/labstack/echo/v4"
8 | "github.com/mikestefanello/pagoda/ent"
9 | "github.com/mikestefanello/pagoda/pkg/services"
10 | "github.com/rs/zerolog/log"
11 | )
12 |
13 | // LoadAuthenticatedUser loads the authenticated user, if one, and stores in context
14 | func SetLastSeenOnline(authClient *services.AuthClient) echo.MiddlewareFunc {
15 | return func(next echo.HandlerFunc) echo.HandlerFunc {
16 | return func(c echo.Context) error {
17 | u, err := authClient.GetAuthenticatedUser(c)
18 | switch err.(type) {
19 | case *ent.NotFoundError:
20 | c.Logger().Warn("auth user not found")
21 | case services.NotAuthenticatedError:
22 | case nil:
23 | err = authClient.SetLastOnlineTimestamp(c, u.ID)
24 | if err != nil {
25 | log.Error().Err(err).Msg("failed to set last seen online")
26 | }
27 | c.Logger().Infof("last seen timestamp set for user: %d", u.ID)
28 | default:
29 | return echo.NewHTTPError(
30 | http.StatusInternalServerError,
31 | fmt.Sprintf("error querying for authenticated user: %v", err),
32 | )
33 | }
34 |
35 | return next(c)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/middleware/log.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/labstack/echo/v4"
7 | )
8 |
9 | // LogRequestID includes the request ID in all logs for the given request
10 | // This requires that middleware that includes the request ID first execute
11 | func LogRequestID() echo.MiddlewareFunc {
12 | return func(next echo.HandlerFunc) echo.HandlerFunc {
13 | return func(c echo.Context) error {
14 | rID := c.Response().Header().Get(echo.HeaderXRequestID)
15 | format := `{"time":"${time_rfc3339_nano}","id":"%s","level":"${level}","prefix":"${prefix}","file":"${short_file}","line":"${line}"}`
16 | c.Logger().SetHeader(fmt.Sprintf(format, rID))
17 | return next(c)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/middleware/log_test.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | // import (
4 | // "bytes"
5 | // "fmt"
6 | // "testing"
7 |
8 | // "github.com/mikestefanello/pagoda/pkg/tests"
9 |
10 | // "github.com/labstack/echo/v4"
11 |
12 | // "github.com/stretchr/testify/assert"
13 |
14 | // echomw "github.com/labstack/echo/v4/middleware"
15 | // )
16 |
17 | // TODO: unskip and fix this test
18 | // func TestLogRequestID(t *testing.T) {
19 | // ctx, _ := tests.NewContext(c.Web, "/")
20 | // _ = tests.ExecuteMiddleware(ctx, echomw.RequestID())
21 | // _ = tests.ExecuteMiddleware(ctx, LogRequestID())
22 |
23 | // var buf bytes.Buffer
24 | // ctx.Logger().SetOutput(&buf)
25 | // ctx.Logger().Info("test")
26 | // rID := ctx.Response().Header().Get(echo.HeaderXRequestID)
27 | // assert.Contains(t, buf.String(), fmt.Sprintf(`id":"%s"`, rID))
28 | // }
29 |
--------------------------------------------------------------------------------
/pkg/middleware/middleware_test.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "github.com/mikestefanello/pagoda/config"
8 | "github.com/mikestefanello/pagoda/ent"
9 | "github.com/mikestefanello/pagoda/pkg/services"
10 | "github.com/mikestefanello/pagoda/pkg/tests"
11 | )
12 |
13 | var (
14 | c *services.Container
15 | usr *ent.User
16 | )
17 |
18 | func TestMain(m *testing.M) {
19 | // Set the environment to test
20 | config.SwitchEnvironment(config.EnvTest)
21 |
22 | // Create a new container
23 | c = services.NewContainer()
24 |
25 | // Create a user
26 | var err error
27 | if usr, err = tests.CreateRandomUser(c.ORM); err != nil {
28 | panic(err)
29 | }
30 |
31 | // Run tests
32 | exitVal := m.Run()
33 |
34 | // Shutdown the container
35 | if err = c.Shutdown(); err != nil {
36 | panic(err)
37 | }
38 |
39 | os.Exit(exitVal)
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/middleware/onboarding.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/mikestefanello/pagoda/pkg/context"
8 | "github.com/mikestefanello/pagoda/pkg/routing/routenames"
9 | )
10 |
11 | func RedirectToOnboardingIfNotComplete() echo.MiddlewareFunc {
12 | return func(next echo.HandlerFunc) echo.HandlerFunc {
13 | return func(c echo.Context) error {
14 | if c.Get(context.ProfileFullyOnboarded) == nil {
15 | return echo.NewHTTPError(http.StatusInternalServerError)
16 | }
17 | isFullyOnboarded := c.Get(context.ProfileFullyOnboarded).(bool)
18 | if !isFullyOnboarded {
19 | url := c.Echo().Reverse(routenames.RouteNamePreferences)
20 | return c.Redirect(303, url)
21 | }
22 | return next(c)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/middleware/sentry.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/getsentry/sentry-go"
7 | "github.com/labstack/echo/v4"
8 | )
9 |
10 | func FilterSentryErrors(next echo.HandlerFunc) echo.HandlerFunc {
11 | return func(c echo.Context) error {
12 | err := next(c)
13 | if err != nil {
14 | // Log error without forwarding to Sentry if it's a 404
15 | httpErr, ok := err.(*echo.HTTPError)
16 | if ok && httpErr.Code == http.StatusNotFound {
17 | // Log or handle the error as you wish, but don't forward to Sentry
18 | c.Logger().Error(err)
19 | return err
20 | }
21 | // Forward other errors to Sentry
22 | sentry.CaptureException(err)
23 | }
24 | return err
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/repos/mailer/resend.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/resend/resend-go/v2"
8 | )
9 |
10 | type ResendMailClient struct {
11 | client *resend.Client
12 | }
13 |
14 | func NewResendMailClient(apiKey string) *ResendMailClient {
15 | client := resend.NewClient(apiKey)
16 | return &ResendMailClient{client: client}
17 | }
18 |
19 | func (r *ResendMailClient) Send(email *mail) error {
20 | params := &resend.SendEmailRequest{
21 | To: []string{email.to},
22 | From: email.from,
23 | Subject: email.subject,
24 | }
25 |
26 | if email.component != nil {
27 | params.Html = email.body
28 | } else {
29 | params.Text = email.body
30 | }
31 |
32 | ctx := context.TODO()
33 | _, err := r.client.Emails.SendWithContext(ctx, params)
34 | if err != nil {
35 | return fmt.Errorf("resend mail client failed to send email: %w", err)
36 | }
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/repos/mailer/smtp.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import (
4 | "fmt"
5 | "net/smtp"
6 | )
7 |
8 | type SMTPMailClient struct {
9 | Port int
10 | Host string
11 | }
12 |
13 | func NewSMTPMailClient(host string, port int) *SMTPMailClient {
14 | return &SMTPMailClient{
15 | Port: port,
16 | Host: host,
17 | }
18 | }
19 |
20 | // Send sends an email using SMTP
21 | func (c *SMTPMailClient) Send(email *mail) error {
22 | // Define email headers and body
23 | msg := fmt.Sprintf("From: %s\nTo: %s\nSubject: %s\nMIME-Version: 1.0\nContent-Type: text/html; charset=\"utf-8\"\n\n%s", email.from, email.to, email.subject, email.body)
24 |
25 | // SMTP server configuration
26 | smtpHost := c.Host
27 | smtpPort := c.Port
28 | smtpAddr := fmt.Sprintf("%s:%d", smtpHost, smtpPort)
29 |
30 | // Authentication - Mailpit does not require authentication, but you could add it here if needed
31 | auth := smtp.PlainAuth("", "", "", smtpHost)
32 |
33 | // Sending email
34 | err := smtp.SendMail(smtpAddr, auth, email.from, []string{email.to}, []byte(msg))
35 | if err != nil {
36 | return err
37 | }
38 |
39 | return nil
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/repos/msg/msg_test.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mikestefanello/pagoda/pkg/tests"
7 |
8 | "github.com/stretchr/testify/assert"
9 | "github.com/stretchr/testify/require"
10 |
11 | "github.com/labstack/echo/v4"
12 | )
13 |
14 | func TestMsg(t *testing.T) {
15 | e := echo.New()
16 | ctx, _ := tests.NewContext(e, "/")
17 | tests.InitSession(ctx)
18 |
19 | assertMsg := func(typ Type, message string) {
20 | ret := Get(ctx, typ)
21 | require.Len(t, ret, 1)
22 | assert.Equal(t, message, ret[0])
23 | ret = Get(ctx, typ)
24 | require.Len(t, ret, 0)
25 | }
26 |
27 | text := "aaa"
28 | Success(ctx, text)
29 | assertMsg(TypeSuccess, text)
30 |
31 | text = "bbb"
32 | Info(ctx, text)
33 | assertMsg(TypeInfo, text)
34 |
35 | text = "ccc"
36 | Danger(ctx, text)
37 | assertMsg(TypeDanger, text)
38 |
39 | text = "ddd"
40 | Warning(ctx, text)
41 | assertMsg(TypeWarning, text)
42 |
43 | text = "eee"
44 | Set(ctx, TypeSuccess, text)
45 | assertMsg(TypeSuccess, text)
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/repos/notifierrepo/push_notifications_fcm_test.go:
--------------------------------------------------------------------------------
1 | package notifierrepo_test
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/mikestefanello/pagoda/ent/notificationpermission"
9 | "github.com/mikestefanello/pagoda/pkg/domain"
10 | "github.com/mikestefanello/pagoda/pkg/repos/notifierrepo"
11 | "github.com/mikestefanello/pagoda/pkg/repos/profilerepo"
12 | storagerepo "github.com/mikestefanello/pagoda/pkg/repos/storage"
13 | "github.com/mikestefanello/pagoda/pkg/repos/subscriptions"
14 | "github.com/mikestefanello/pagoda/pkg/tests"
15 | "github.com/stretchr/testify/assert"
16 | )
17 |
18 | func TestFcmHasPermissionsLeftAndTokenIsRegistered(t *testing.T) {
19 | client, ctx := tests.CreateTestContainerPostgresEntClient(t)
20 | defer client.Close()
21 |
22 | // Create user and profile.
23 | user1 := tests.CreateUser(ctx, client, "User", "user1@example.com", "password", true)
24 | subscriptionsRepo := subscriptions.NewSubscriptionsRepo(client, 10, 10)
25 | profileRepo := profilerepo.NewProfileRepo(client, storagerepo.NewMockStorageClient(), subscriptionsRepo)
26 |
27 | profile1, err := profileRepo.CreateProfile(
28 | ctx, user1, "bio",
29 | time.Now().AddDate(-25, 0, 0), nil, nil,
30 | )
31 | assert.Nil(t, err)
32 |
33 | // Set permissions
34 | uuidToken, err := uuid.NewV7(uuid.MicrosecondPrecision)
35 | assert.NoError(t, err)
36 | _, err = client.NotificationPermission.Create().
37 | SetProfileID(profile1.ID).
38 | SetPermission(notificationpermission.Permission(domain.NotificationPermissionDailyReminder.Value)).
39 | SetPlatform(notificationpermission.Platform(domain.NotificationPlatformPush.Value)).
40 | SetToken(uuidToken.String()).
41 | Save(ctx)
42 | assert.Nil(t, err)
43 |
44 | fcmPushNotificationsRepo, err := notifierrepo.NewFcmPushNotificationsRepo(client, nil)
45 | assert.Nil(t, err)
46 |
47 | err = fcmPushNotificationsRepo.AddPushSubscription(ctx, profile1.ID, notifierrepo.FcmSubscription{
48 | Token: "12345",
49 | })
50 | assert.Nil(t, err)
51 |
52 | hasPermissionsLeft, err := fcmPushNotificationsRepo.HasPermissionsLeftAndTokenIsRegistered(ctx, profile1.ID, "12345")
53 | assert.Nil(t, err)
54 | // TODO: the below is False even though it would normally be True, because
55 | // I did not explicitly add a permission, and fcmPushNotificationsRepo is breaking
56 | // walls of responsability by putting its hands in permissions. Bad design. To rework.
57 | assert.False(t, hasPermissionsLeft)
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/routing/routes/about.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/pkg/types"
6 | "github.com/mikestefanello/pagoda/templates"
7 | "github.com/mikestefanello/pagoda/templates/layouts"
8 | "github.com/mikestefanello/pagoda/templates/pages"
9 |
10 | "github.com/labstack/echo/v4"
11 | )
12 |
13 | type (
14 | about struct {
15 | ctr controller.Controller
16 | }
17 | )
18 |
19 | func NewAboutUsRoute(ctr controller.Controller) about {
20 | return about{
21 | ctr: ctr,
22 | }
23 | }
24 |
25 | func (c *about) Get(ctx echo.Context) error {
26 |
27 | page := controller.NewPage(ctx)
28 | page.Layout = layouts.Main
29 | page.Name = templates.PageAbout
30 | page.Component = pages.About(&page)
31 | page.Data = types.AboutData{
32 | SupportEmail: c.ctr.Container.Config.App.SupportEmail,
33 | }
34 | page.HTMX.Request.Boosted = true
35 |
36 | return c.ctr.RenderPage(ctx, page)
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/routing/routes/about_test.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | routeNames "github.com/mikestefanello/pagoda/pkg/routing/routenames"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | // Simple example of how to test routes and their markup using the test HTTP server spun up within
12 | // this test package
13 | func TestAbout_Get(t *testing.T) {
14 | t.Skip("Skipping TestAbout_Get for now")
15 |
16 | doc := request(t).
17 | setRoute(routeNames.RouteNameAboutUs).
18 | get().
19 | assertStatusCode(http.StatusOK).
20 | toDoc()
21 |
22 | // Goquery is an excellent package to use for testing HTML markup
23 | h1 := doc.Find("h1.title")
24 | assert.Len(t, h1.Nodes, 1)
25 | assert.Equal(t, "About", h1.Text())
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/routing/routes/clear_site_cookie.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/mikestefanello/pagoda/pkg/controller"
8 | "github.com/mikestefanello/pagoda/pkg/repos/msg"
9 | "github.com/mikestefanello/pagoda/pkg/routing/routenames"
10 | )
11 |
12 | type (
13 | clearCookie struct {
14 | ctr controller.Controller
15 | }
16 | )
17 |
18 | func NewClearCookiesRoute(ctr controller.Controller) clearCookie {
19 | return clearCookie{
20 | ctr: ctr,
21 | }
22 | }
23 |
24 | func (ck *clearCookie) Get(ctx echo.Context) error {
25 | if err := ck.ctr.Container.Auth.Logout(ctx); err == nil {
26 | msg.Success(ctx, "You have successfully cleared this site's cookie.")
27 | } else {
28 | msg.Danger(ctx, "An error occurred. Please try again.")
29 | }
30 |
31 | // Clear all other cookies
32 | for _, cookie := range ctx.Cookies() {
33 | cookie.Expires = time.Now().UTC().Add(-100 * time.Hour) // Set to a time in the past
34 | cookie.MaxAge = -1
35 | ctx.SetCookie(cookie)
36 | }
37 | return ck.ctr.Redirect(ctx, routenames.RouteNameLogin)
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/routing/routes/contact.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/mikestefanello/pagoda/pkg/context"
7 | "github.com/mikestefanello/pagoda/pkg/controller"
8 | "github.com/mikestefanello/pagoda/pkg/repos/msg"
9 |
10 | "github.com/mikestefanello/pagoda/pkg/types"
11 | "github.com/mikestefanello/pagoda/templates"
12 | "github.com/mikestefanello/pagoda/templates/layouts"
13 | "github.com/mikestefanello/pagoda/templates/pages"
14 |
15 | "github.com/labstack/echo/v4"
16 | )
17 |
18 | type (
19 | contact struct {
20 | controller.Controller
21 | }
22 | )
23 |
24 | func (c *contact) Get(ctx echo.Context) error {
25 | page := controller.NewPage(ctx)
26 | page.Layout = layouts.Main
27 | page.Name = templates.PageContact
28 | page.Title = "Contact us"
29 | page.Form = &types.ContactForm{}
30 | page.Component = pages.Contact(&page)
31 | page.HTMX.Request.Boosted = true
32 |
33 | if form := ctx.Get(context.FormKey); form != nil {
34 | page.Form = form.(*types.ContactForm)
35 | }
36 | msg.Success(ctx, "Success!")
37 | msg.Warning(ctx, "Warning!")
38 | msg.Danger(ctx, "Danger!")
39 | msg.Info(ctx, "Info!")
40 |
41 | return c.RenderPage(ctx, page)
42 | }
43 |
44 | func (c *contact) Post(ctx echo.Context) error {
45 | var form types.ContactForm
46 | ctx.Set(context.FormKey, &form)
47 |
48 | // Parse the form values
49 | if err := ctx.Bind(&form); err != nil {
50 | return c.Fail(err, "unable to bind form")
51 | }
52 |
53 | if err := form.Submission.Process(ctx, form); err != nil {
54 | return c.Fail(err, "unable to process form submission")
55 | }
56 |
57 | if !form.Submission.HasErrors() {
58 | err := c.Container.Mail.
59 | Compose().
60 | To(form.Email).
61 | Subject("Contact form submitted").
62 | Body(fmt.Sprintf("The message is: %s", form.Message)).
63 | Send(ctx.Request().Context())
64 |
65 | if err != nil {
66 | return c.Fail(err, "unable to send email")
67 | }
68 | }
69 |
70 | return c.Get(ctx)
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/routing/routes/docs.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/mikestefanello/pagoda/pkg/controller"
6 | "github.com/mikestefanello/pagoda/templates"
7 | "github.com/mikestefanello/pagoda/templates/layouts"
8 | "github.com/mikestefanello/pagoda/templates/pages"
9 | )
10 |
11 | type docsRoute struct {
12 | ctr controller.Controller
13 | }
14 |
15 | func NewDocsRoute(ctr controller.Controller) *docsRoute {
16 | return &docsRoute{
17 | ctr: ctr,
18 | }
19 | }
20 |
21 | func (w *docsRoute) GetDocsHome(ctx echo.Context) error {
22 | page := controller.NewPage(ctx)
23 | page.Layout = layouts.Documentation
24 | page.Name = templates.PageWiki
25 | page.Title = "Introduction"
26 | page.Component = pages.DocumentationLandingPage(&page)
27 | page.HTMX.Request.Boosted = true
28 |
29 | return w.ctr.RenderPage(ctx, page)
30 | }
31 |
32 | func (w *docsRoute) GetDocsGettingStarted(ctx echo.Context) error {
33 | page := controller.NewPage(ctx)
34 | page.Layout = layouts.Documentation
35 | page.Name = templates.PageWiki
36 | page.Title = "Architecture"
37 | page.Component = pages.DocumentationArchitecturePage(&page)
38 | page.HTMX.Request.Boosted = true
39 |
40 | return w.ctr.RenderPage(ctx, page)
41 | }
42 |
43 | func (w *docsRoute) GetDocsGuidedTour(ctx echo.Context) error {
44 | page := controller.NewPage(ctx)
45 | page.Layout = layouts.Documentation
46 | page.Name = templates.PageWiki
47 | page.Title = "Architecture"
48 | page.Component = pages.DocumentationArchitecturePage(&page)
49 | page.HTMX.Request.Boosted = true
50 |
51 | return w.ctr.RenderPage(ctx, page)
52 | }
53 |
54 | func (w *docsRoute) GetDocsArchitecture(ctx echo.Context) error {
55 | page := controller.NewPage(ctx)
56 | page.Layout = layouts.Documentation
57 | page.Name = templates.PageWiki
58 | page.Title = "Architecture"
59 | page.Component = pages.DocumentationArchitecturePage(&page)
60 | page.HTMX.Request.Boosted = true
61 |
62 | return w.ctr.RenderPage(ctx, page)
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/routing/routes/error.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/mikestefanello/pagoda/pkg/context"
7 | "github.com/mikestefanello/pagoda/pkg/controller"
8 | "github.com/mikestefanello/pagoda/templates"
9 | "github.com/mikestefanello/pagoda/templates/layouts"
10 | "github.com/mikestefanello/pagoda/templates/pages"
11 |
12 | "github.com/labstack/echo/v4"
13 | )
14 |
15 | type errorHandler struct {
16 | ctr controller.Controller
17 | }
18 |
19 | func NewErrorHandler(ctr controller.Controller) errorHandler {
20 | return errorHandler{
21 | ctr: ctr,
22 | }
23 | }
24 |
25 | func (e *errorHandler) Get(err error, ctx echo.Context) {
26 | if ctx.Response().Committed || context.IsCanceledError(err) {
27 | return
28 | }
29 |
30 | code := http.StatusInternalServerError
31 | if he, ok := err.(*echo.HTTPError); ok {
32 | code = he.Code
33 | }
34 |
35 | if code >= 500 {
36 | ctx.Logger().Error(err)
37 | } else {
38 | ctx.Logger().Info(err)
39 | }
40 |
41 | page := controller.NewPage(ctx)
42 | // page.Title = http.StatusText(code)
43 | page.Layout = layouts.Main
44 | page.Name = templates.PageError
45 | page.StatusCode = code
46 | page.HTMX.Request.Enabled = false
47 | page.HTMX.Request.Boosted = true
48 |
49 | page.Component = pages.Error(&page)
50 |
51 | if err = e.ctr.RenderPage(ctx, page); err != nil {
52 | ctx.Logger().Error(err)
53 | }
54 | }
55 |
56 | func (e *errorHandler) GetHttp400BadRequest(ctx echo.Context) error {
57 | e.Get(echo.NewHTTPError(http.StatusBadRequest, "Bad Request"), ctx)
58 | return nil
59 | }
60 |
61 | func (e *errorHandler) GetHttp401Unauthorized(ctx echo.Context) error {
62 | e.Get(echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized"), ctx)
63 | return nil
64 | }
65 |
66 | func (e *errorHandler) GetHttp403Forbidden(ctx echo.Context) error {
67 | e.Get(echo.NewHTTPError(http.StatusForbidden, "Forbidden"), ctx)
68 | return nil
69 | }
70 |
71 | func (e *errorHandler) GetHttp404NotFound(ctx echo.Context) error {
72 | e.Get(echo.NewHTTPError(http.StatusNotFound, "Not Found"), ctx)
73 | return nil
74 | }
75 |
76 | func (e *errorHandler) GetHttp500InternalServerError(ctx echo.Context) error {
77 | e.Get(echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error"), ctx)
78 | return nil
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/routing/routes/healthcheck.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 |
6 | "github.com/mikestefanello/pagoda/pkg/controller"
7 | "github.com/mikestefanello/pagoda/templates"
8 | "github.com/mikestefanello/pagoda/templates/layouts"
9 | "github.com/mikestefanello/pagoda/templates/pages"
10 | )
11 |
12 | type (
13 | healthcheck struct {
14 | ctr controller.Controller
15 | }
16 | )
17 |
18 | func NewHealthCheckRoute(ctr controller.Controller) healthcheck {
19 | return healthcheck{
20 | ctr: ctr,
21 | }
22 | }
23 |
24 | func (g *healthcheck) Get(ctx echo.Context) error {
25 | page := controller.NewPage(ctx)
26 | page.Layout = layouts.Main
27 | page.Name = templates.PageHealthcheck
28 | page.Component = pages.HealthCheck(&page)
29 | page.Cache.Enabled = false
30 |
31 | return g.ctr.RenderPage(ctx, page)
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/routing/routes/helpers.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import "net/url"
4 |
5 | // AddQueryParam takes a URL, key, and value and returns the URL with the added query parameter.
6 | func AddQueryParam(urlStr, key, value string) (string, error) {
7 | // Parse the URL to get a url.URL struct.
8 | parsedURL, err := url.Parse(urlStr)
9 | if err != nil {
10 | return "", err
11 | }
12 |
13 | // Create a Values object from the parsed URL's query string.
14 | values := parsedURL.Query()
15 |
16 | // Add the key-value pair to the URL query parameters.
17 | values.Add(key, value)
18 |
19 | // Encode the query parameters and assign it back to the URL's RawQuery.
20 | parsedURL.RawQuery = values.Encode()
21 |
22 | // Return the updated URL as a string.
23 | return parsedURL.String(), nil
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/routing/routes/home.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/mikestefanello/pagoda/pkg/controller"
7 | "github.com/mikestefanello/pagoda/pkg/types"
8 | "github.com/mikestefanello/pagoda/templates"
9 | "github.com/mikestefanello/pagoda/templates/layouts"
10 | "github.com/mikestefanello/pagoda/templates/pages"
11 |
12 | "github.com/labstack/echo/v4"
13 | )
14 |
15 | type (
16 | home struct {
17 | controller.Controller
18 | }
19 | )
20 |
21 | func (c *home) Get(ctx echo.Context) error {
22 | page := controller.NewPage(ctx)
23 |
24 | if page.AuthUser != nil {
25 | return c.Redirect(ctx, "dashboard")
26 |
27 | }
28 |
29 | page.Layout = layouts.Main
30 | page.Name = templates.PageHome
31 | page.Metatags.Description = "Welcome to the homepage."
32 | page.Metatags.Keywords = []string{"Go", "MVC", "Web", "Software"}
33 | page.Pager = controller.NewPager(ctx, 4)
34 | page.Data = c.fetchPosts(&page.Pager)
35 | page.Component = pages.Home(&page)
36 | page.HTMX.Request.Boosted = true
37 |
38 | return c.RenderPage(ctx, page)
39 | }
40 |
41 | // fetchPosts is an mock example of fetching posts to illustrate how paging works
42 | func (c *home) fetchPosts(pager *controller.Pager) []types.Post {
43 | pager.SetItems(20)
44 | posts := make([]types.Post, 20)
45 |
46 | for k := range posts {
47 | posts[k] = types.Post{
48 | Title: fmt.Sprintf("Post example #%d", k+1),
49 | Body: fmt.Sprintf("Lorem ipsum example #%d ddolor sit amet, consectetur adipiscing elit. Nam elementum vulputate tristique.", k+1),
50 | }
51 | }
52 |
53 | return posts[pager.GetOffset() : pager.GetOffset()+pager.ItemsPerPage]
54 | }
55 |
--------------------------------------------------------------------------------
/pkg/routing/routes/install_app.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/mikestefanello/pagoda/pkg/controller"
6 | "github.com/mikestefanello/pagoda/templates"
7 | "github.com/mikestefanello/pagoda/templates/layouts"
8 | "github.com/mikestefanello/pagoda/templates/pages"
9 | )
10 |
11 | type (
12 | installApp struct {
13 | ctr controller.Controller
14 | }
15 | )
16 |
17 | func NewInstallAppRoute(
18 | ctr controller.Controller,
19 | ) installApp {
20 | return installApp{
21 | ctr: ctr,
22 | }
23 | }
24 |
25 | func (c *installApp) GetInstallPage(ctx echo.Context) error {
26 | page := controller.NewPage(ctx)
27 | page.Layout = layouts.Main
28 | page.Name = templates.PageInstallApp
29 | page.Component = pages.InstallApp(&page)
30 | page.HTMX.Request.Boosted = true
31 |
32 | return c.ctr.RenderPage(ctx, page)
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/routing/routes/logout.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/pkg/repos/msg"
6 | routeNames "github.com/mikestefanello/pagoda/pkg/routing/routenames"
7 |
8 | "github.com/labstack/echo/v4"
9 | )
10 |
11 | type logout struct {
12 | ctr controller.Controller
13 | }
14 |
15 | func NewLogoutRoute(ctr controller.Controller) *logout {
16 | return &logout{ctr: ctr}
17 | }
18 |
19 | func (l *logout) Get(c echo.Context) error {
20 | if err := l.ctr.Container.Auth.Logout(c); err == nil {
21 |
22 | } else {
23 | msg.Danger(c, "An error occurred. Please try again.")
24 | }
25 | return l.ctr.Redirect(c, routeNames.RouteNameLandingPage)
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/routing/routes/privacy.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/pkg/types"
6 | "github.com/mikestefanello/pagoda/templates"
7 | "github.com/mikestefanello/pagoda/templates/layouts"
8 | "github.com/mikestefanello/pagoda/templates/pages"
9 |
10 | "github.com/labstack/echo/v4"
11 | )
12 |
13 | type (
14 | privacyPolicy struct {
15 | ctr controller.Controller
16 | }
17 | )
18 |
19 | func NewPrivacyPolicyRoute(ctr controller.Controller) privacyPolicy {
20 | return privacyPolicy{
21 | ctr: ctr,
22 | }
23 | }
24 |
25 | func (c *privacyPolicy) Get(ctx echo.Context) error {
26 |
27 | page := controller.NewPage(ctx)
28 | page.Layout = layouts.Main
29 | page.Name = templates.PagePrivacyPolicy
30 | page.Component = pages.PrivacyPolicy(&page)
31 | page.Data = types.AboutData{
32 | SupportEmail: c.ctr.Container.Config.App.SupportEmail,
33 | }
34 |
35 | page.HTMX.Request.Boosted = true
36 |
37 | return c.ctr.RenderPage(ctx, page)
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/routing/routes/verify_email.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/mikestefanello/pagoda/ent"
6 | "github.com/mikestefanello/pagoda/ent/user"
7 | "github.com/mikestefanello/pagoda/pkg/context"
8 | "github.com/mikestefanello/pagoda/pkg/controller"
9 | "github.com/mikestefanello/pagoda/pkg/repos/msg"
10 | routeNames "github.com/mikestefanello/pagoda/pkg/routing/routenames"
11 | )
12 |
13 | type verifyEmail struct {
14 | ctr controller.Controller
15 | }
16 |
17 | func NewVerifyEmailRoute(ctr controller.Controller) *verifyEmail {
18 | return &verifyEmail{ctr: ctr}
19 | }
20 | func (c *verifyEmail) Get(ctx echo.Context) error {
21 | var usr *ent.User
22 |
23 | // Validate the token
24 | token := ctx.Param("token")
25 | email, err := c.ctr.Container.Auth.ValidateEmailVerificationToken(token)
26 | if err != nil {
27 | msg.Warning(ctx, "The link is either invalid or has expired.")
28 | return c.ctr.Redirect(ctx, routeNames.RouteNameLandingPage)
29 | }
30 |
31 | // Check if it matches the authenticated user
32 | u := ctx.Get(context.AuthenticatedUserKey)
33 | if u != nil {
34 | authUser := u.(*ent.User)
35 |
36 | if authUser.Email == email {
37 | usr = authUser
38 | }
39 | }
40 |
41 | // Query to find a matching user, if needed
42 | if usr == nil {
43 | usr, err = c.ctr.Container.ORM.User.
44 | Query().
45 | Where(user.Email(email)).
46 | Only(ctx.Request().Context())
47 |
48 | if err != nil {
49 | return c.ctr.Fail(err, "query failed loading email verification token user")
50 | }
51 | }
52 |
53 | // Verify the user, if needed
54 | if !usr.Verified {
55 | _, err = usr.
56 | Update().
57 | SetVerified(true).
58 | Save(ctx.Request().Context())
59 |
60 | if err != nil {
61 | return c.ctr.Fail(err, "failed to set user as verified")
62 | }
63 | }
64 |
65 | msg.Success(ctx, "Your email has been successfully verified.")
66 |
67 | // If we have a user, they are already logged in and just redirect them to their home feed
68 | if u != nil {
69 | return c.ctr.Redirect(ctx, routeNames.RouteNamePreferences)
70 |
71 | }
72 | return c.ctr.Redirect(ctx, routeNames.RouteNameLogin)
73 | }
74 |
--------------------------------------------------------------------------------
/pkg/routing/routes/verify_email_subscription.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/mikestefanello/pagoda/pkg/controller"
6 | "github.com/mikestefanello/pagoda/pkg/repos/emailsmanager"
7 | "github.com/mikestefanello/pagoda/templates/layouts"
8 | )
9 |
10 | type verifyEmailSubscription struct {
11 | ctr controller.Controller
12 | emailSubscriptionRepo emailsmanager.EmailSubscriptionRepo
13 | }
14 |
15 | func NewVerifyEmailSubscriptionRoute(
16 | ctr controller.Controller, emailSubscriptionRepo emailsmanager.EmailSubscriptionRepo,
17 | ) verifyEmailSubscription {
18 |
19 | return verifyEmailSubscription{
20 | ctr: ctr,
21 | emailSubscriptionRepo: emailSubscriptionRepo,
22 | }
23 | }
24 |
25 | type SubscriptionData struct {
26 | Suceeded bool
27 | SignupEnabled bool
28 | }
29 |
30 | func (c *verifyEmailSubscription) Get(ctx echo.Context) error {
31 | page := controller.NewPage(ctx)
32 | page.Layout = layouts.Main
33 | page.Name = "subscribe-confirmation"
34 |
35 | // Validate the token
36 | token := ctx.Param("token")
37 |
38 | err := c.emailSubscriptionRepo.ConfirmSubscription(ctx.Request().Context(), token)
39 | if err != nil {
40 | page.Data = SubscriptionData{Suceeded: false, SignupEnabled: false}
41 | } else {
42 | page.Data = SubscriptionData{Suceeded: true, SignupEnabled: false}
43 | }
44 |
45 | return c.ctr.RenderPage(ctx, page)
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/services/cache_test.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 |
8 | "github.com/go-redis/redis/v8"
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestCacheClient(t *testing.T) {
14 | type cacheTest struct {
15 | Value string
16 | }
17 | // Cache some data
18 | data := cacheTest{Value: "abcdef"}
19 | group := "testgroup"
20 | key := "testkey"
21 | err := c.Cache.
22 | Set().
23 | Group(group).
24 | Key(key).
25 | Data(data).
26 | Save(context.Background())
27 | require.NoError(t, err)
28 |
29 | // Get the data
30 | fromCache, err := c.Cache.
31 | Get().
32 | Group(group).
33 | Key(key).
34 | Type(new(cacheTest)).
35 | Fetch(context.Background())
36 | require.NoError(t, err)
37 | cast, ok := fromCache.(*cacheTest)
38 | require.True(t, ok)
39 | assert.Equal(t, data, *cast)
40 |
41 | // The same key with the wrong group should fail
42 | _, err = c.Cache.
43 | Get().
44 | Key(key).
45 | Type(new(cacheTest)).
46 | Fetch(context.Background())
47 | assert.Error(t, err)
48 |
49 | // Flush the data
50 | err = c.Cache.
51 | Flush().
52 | Group(group).
53 | Key(key).
54 | Execute(context.Background())
55 | require.NoError(t, err)
56 |
57 | // The data should be gone
58 | assertFlushed := func() {
59 | // The data should be gone
60 | _, err = c.Cache.
61 | Get().
62 | Group(group).
63 | Key(key).
64 | Type(new(cacheTest)).
65 | Fetch(context.Background())
66 | assert.Equal(t, redis.Nil, err)
67 | }
68 | assertFlushed()
69 |
70 | // Set with tags
71 | err = c.Cache.
72 | Set().
73 | Group(group).
74 | Key(key).
75 | Data(data).
76 | Tags("tag1").
77 | Save(context.Background())
78 | require.NoError(t, err)
79 |
80 | // Flush the tag
81 | err = c.Cache.
82 | Flush().
83 | Tags("tag1").
84 | Execute(context.Background())
85 | require.NoError(t, err)
86 |
87 | // The data should be gone
88 | assertFlushed()
89 |
90 | // Set with expiration
91 | err = c.Cache.
92 | Set().
93 | Group(group).
94 | Key(key).
95 | Data(data).
96 | Expiration(time.Millisecond).
97 | Save(context.Background())
98 | require.NoError(t, err)
99 |
100 | // Wait for expiration
101 | time.Sleep(time.Millisecond * 2)
102 |
103 | // The data should be gone
104 | assertFlushed()
105 | }
106 |
--------------------------------------------------------------------------------
/pkg/services/container_test.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestNewContainer(t *testing.T) {
10 | assert.NotNil(t, c.Web)
11 | assert.NotNil(t, c.Config)
12 | assert.NotNil(t, c.Validator)
13 | assert.NotNil(t, c.Cache)
14 | assert.NotNil(t, c.Database)
15 | assert.NotNil(t, c.ORM)
16 | assert.NotNil(t, c.Mail)
17 | assert.NotNil(t, c.Auth)
18 | assert.NotNil(t, c.Tasks)
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/services/services_test.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "github.com/mikestefanello/pagoda/config"
8 | "github.com/mikestefanello/pagoda/ent"
9 | "github.com/mikestefanello/pagoda/pkg/tests"
10 |
11 | "github.com/labstack/echo/v4"
12 | )
13 |
14 | var (
15 | c *Container
16 | ctx echo.Context
17 | usr *ent.User
18 | )
19 |
20 | func TestMain(m *testing.M) {
21 | // Set the environment to test
22 | config.SwitchEnvironment(config.EnvTest)
23 |
24 | // Create a new container
25 | c = NewContainer()
26 |
27 | // Create a web context
28 | ctx, _ = tests.NewContext(c.Web, "/")
29 | tests.InitSession(ctx)
30 |
31 | // Create a test user
32 | var err error
33 | if usr, err = tests.CreateRandomUser(c.ORM); err != nil {
34 | panic(err)
35 | }
36 |
37 | // Run tests
38 | exitVal := m.Run()
39 |
40 | // Shutdown the container
41 | if err = c.Shutdown(); err != nil {
42 | panic(err)
43 | }
44 |
45 | os.Exit(exitVal)
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/services/tasks_test.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestTaskClient_New(t *testing.T) {
11 | now := time.Now()
12 | tk := c.Tasks.
13 | New("task1").
14 | Payload("payload").
15 | Queue("queue").
16 | Periodic("@every 5s").
17 | MaxRetries(5).
18 | Timeout(5 * time.Second).
19 | Deadline(now).
20 | At(now).
21 | Wait(6 * time.Second).
22 | Retain(7 * time.Second)
23 |
24 | assert.Equal(t, "task1", tk.typ)
25 | assert.Equal(t, "payload", tk.payload.(string))
26 | assert.Equal(t, "queue", *tk.queue)
27 | assert.Equal(t, "@every 5s", *tk.periodic)
28 | assert.Equal(t, 5, *tk.maxRetries)
29 | assert.Equal(t, 5*time.Second, *tk.timeout)
30 | assert.Equal(t, now, *tk.deadline)
31 | assert.Equal(t, now, *tk.at)
32 | assert.Equal(t, 6*time.Second, *tk.wait)
33 | assert.Equal(t, 7*time.Second, *tk.retain)
34 | assert.NoError(t, tk.Save())
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/services/validator.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "github.com/go-playground/validator/v10"
5 | )
6 |
7 | // Validator provides validation mainly validating structs within the web context
8 | type Validator struct {
9 | // validator stores the underlying validator
10 | validator *validator.Validate
11 | }
12 |
13 | // NewValidator creats a new Validator
14 | func NewValidator() *Validator {
15 | return &Validator{
16 | validator: validator.New(),
17 | }
18 | }
19 |
20 | // Validate validates a struct
21 | func (v *Validator) Validate(i any) error {
22 | if err := v.validator.Struct(i); err != nil {
23 | return err
24 | }
25 | return nil
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/services/validator_test.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestValidator(t *testing.T) {
10 | type example struct {
11 | Value string `validate:"required"`
12 | }
13 | e := example{}
14 | err := c.Validator.Validate(e)
15 | assert.Error(t, err)
16 | e.Value = "a"
17 | err = c.Validator.Validate(e)
18 | assert.NoError(t, err)
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/types/about.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type (
4 | AboutData struct {
5 | SupportEmail string
6 | }
7 | )
8 |
--------------------------------------------------------------------------------
/pkg/types/committed.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | type (
6 | DropdownIterable struct {
7 | ID int `json:"id"`
8 | Object any `json:"object"`
9 | }
10 |
11 | CommittedModePageData struct {
12 | Friends []DropdownIterable
13 | InvitationText string
14 | InvitationLink string
15 | }
16 |
17 | UpdateInAppModeForm struct {
18 | MatchProfileID int `form:"match_id" validate:"required"`
19 | Submission controller.FormSubmission
20 | }
21 | )
22 |
--------------------------------------------------------------------------------
/pkg/types/contact.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | type (
6 | ContactForm struct {
7 | Email string `form:"email" validate:"required,email"`
8 | Message string `form:"message" validate:"required"`
9 | Submission controller.FormSubmission
10 | }
11 | )
12 |
--------------------------------------------------------------------------------
/pkg/types/email_subscribe.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | type (
6 | EmailSubscriptionData struct {
7 | Description string
8 | Placeholder string
9 | Latitude float64
10 | Longitude float64
11 | }
12 |
13 | EmailSubscriptionForm struct {
14 | Email string `form:"email" validate:"required"`
15 | Latitude float64 `form:"latitude" validate:"required"`
16 | Longitude float64 `form:"longitude" validate:"required"`
17 | Submission controller.FormSubmission
18 | }
19 | )
20 |
--------------------------------------------------------------------------------
/pkg/types/emails.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type (
4 | EmailDefaultData struct {
5 | AppName string
6 | SupportEmail string
7 | Domain string
8 | ConfirmationLink string
9 | }
10 |
11 | EmailPasswordResetData struct {
12 | AppName string
13 | SupportEmail string
14 | Domain string
15 | ProfileName string
16 | PasswordResetLink string
17 | OperatingSystem string
18 | BrowserName string
19 | }
20 |
21 | QuestionInEmail struct {
22 | Question string
23 | WriteAnswerURL string
24 | }
25 |
26 | EmailUpdate struct {
27 | SelfName string
28 | AppName string
29 | SupportEmail string
30 | Domain string
31 | PartnerName string
32 | NumNewNotifications int
33 | QuestionsAnsweredByFriendButNotSelfTitle string
34 | NumQuestionsAnsweredByFriendButNotSelf int
35 | QuestionsAnsweredByFriendButNotSelf []QuestionInEmail
36 | QuestionsNotAnsweredInSocialCircle []QuestionInEmail
37 | UnsubscribeDailyUpdatesLink string
38 | UnsubscribePartnerActivityLink string
39 | }
40 | )
41 |
--------------------------------------------------------------------------------
/pkg/types/forgot_password.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | type (
6 | ForgotPasswordForm struct {
7 | Email string `form:"email" validate:"required,email"`
8 | Submission controller.FormSubmission
9 | }
10 | )
11 |
--------------------------------------------------------------------------------
/pkg/types/home.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type (
4 | Post struct {
5 | Title string
6 | Body string
7 | }
8 | )
9 |
--------------------------------------------------------------------------------
/pkg/types/home_feed.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type (
4 | HomeFeedData struct {
5 | NextPageURL string
6 | RanOutOfQuestions bool
7 | SupportEmail string
8 | NumFriends int
9 | JustFinishedOnboarded bool
10 | }
11 |
12 | HomeFeedButtonsData struct {
13 | NumDrafts int
14 | NumLikedQuestions int
15 | NumWaitingOnPartner int
16 | NumWaitingOnYou int
17 | MaxNumCanWaitOnYou int
18 | }
19 |
20 | HomeFeedStatsData struct {
21 | NumAnsweredInLast24H int
22 | }
23 | )
24 |
--------------------------------------------------------------------------------
/pkg/types/invitations.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type (
4 | InvitationsData struct {
5 | InvitationText string
6 | }
7 | )
8 |
--------------------------------------------------------------------------------
/pkg/types/landing_page.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "html/template"
4 |
5 | type LandingPage struct {
6 | AppName string
7 | UserSignupEnabled bool
8 |
9 | Title string
10 | Subtitle string
11 | GetNowText string
12 |
13 | IntroTitle string
14 | IntroText template.HTML
15 |
16 | OtherAppPitchTitle string
17 | OtherAppPitchText string
18 | OtherAppURL string
19 | OtherAppName string
20 |
21 | HowItWorksTitle string
22 |
23 | Quote1 string
24 | Quote2 string
25 |
26 | ExampleQuestion1 string
27 | ExampleQuestion2 string
28 | ExampleQuestion3 string
29 |
30 | AboutUsTitle1 string
31 | AboutUsText1 string
32 | AboutUsTitle2 string
33 | AboutUsText2 string
34 |
35 | QAItems []QAItem
36 |
37 | HeroSmImageURL string
38 | HeroMdImageURL string
39 | HeroLgImageURL string
40 | BackgroundPhoto2lg string
41 | BackgroundPhoto2xl string
42 |
43 | IsPaymentEnabled bool
44 | ProductProCode string
45 | ProductProPrice string
46 |
47 | ContactEmail string
48 | }
49 |
50 | type QAItem struct {
51 | Question string
52 | Answer string
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/types/login.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | type (
6 | LoginForm struct {
7 | Email string `form:"email" validate:"required,email"`
8 | Password string `form:"password" validate:"required"`
9 | Submission controller.FormSubmission
10 | }
11 | )
12 |
--------------------------------------------------------------------------------
/pkg/types/notifications.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/domain"
4 |
5 | type NormalNotificationsPageData struct {
6 | Notifications []*domain.Notification
7 | NextPageURL string
8 | }
9 |
--------------------------------------------------------------------------------
/pkg/types/page_data.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/mikestefanello/pagoda/ent"
4 |
5 | type PageData struct {
6 | IsAuth bool
7 | AuthUser *ent.User
8 | Data any
9 | ToURL func(name string, params ...any) string
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/types/payments.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/mikestefanello/pagoda/pkg/controller"
7 | "github.com/mikestefanello/pagoda/pkg/domain"
8 | )
9 |
10 | type (
11 | PaymentProcessorPublicKey struct {
12 | Key string `json:"key"`
13 | }
14 |
15 | CreateCheckoutSessionForm struct {
16 | Submission controller.FormSubmission
17 | PriceID string `form:"price_id", validate:required`
18 | }
19 |
20 | ProductDescription struct {
21 | Name string
22 | Subtitle string
23 | Price string
24 | Points []string
25 | ProductType domain.ProductType
26 | }
27 | PricingPageData struct {
28 | ProductProCode string
29 | ProductProPrice string
30 | ActivePlan domain.ProductType
31 | IsTrial bool
32 | SubscriptionExpiresOn *time.Time
33 | ProductDescriptions []ProductDescription
34 | }
35 | )
36 |
--------------------------------------------------------------------------------
/pkg/types/profile.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/domain"
5 | )
6 |
7 | type ProfilePageData struct {
8 | Profile domain.Profile
9 | PhotosJSON string
10 | IsSelf bool
11 | ShowGenderAndAge bool
12 | UploadProfilePicUrl string
13 | UploadGalleryPicUrl string
14 | CanUploadMoreGalleryPics bool
15 | GalleryPicsMaxCount int
16 | JustAcceptedCommitedRequest bool
17 | }
18 |
19 | type ProfileCalendarHeatmap struct {
20 | Counts []CountByDay
21 | }
22 | type CountByDay struct {
23 | Date string `json:"date"`
24 | Value int `json:"value"`
25 | }
26 |
27 | type LocalizationPageData struct {
28 | Latitude float64
29 | Longitude float64
30 | Radius int
31 | PostGeoDataURL string
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/types/register.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | type (
6 | RegisterForm struct {
7 | RelationshipStatus string `form:"relationship_status" validate:"required"`
8 | Name string `form:"name" validate:"required"`
9 | Email string `form:"email" validate:"required,email"`
10 | Password string `form:"password" validate:"required"`
11 | Birthdate string `form:"birthdate" validate:"required"`
12 | Submission controller.FormSubmission
13 | }
14 |
15 | RegisterData struct {
16 | RelationshipStatus string
17 | UserSignupEnabled bool
18 | MinDate string
19 | }
20 | )
21 |
--------------------------------------------------------------------------------
/pkg/types/reset_password.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | type (
6 | ResetPasswordForm struct {
7 | Password string `form:"password" validate:"required"`
8 | ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"`
9 | Submission controller.FormSubmission
10 | }
11 | )
12 |
--------------------------------------------------------------------------------
/pkg/types/search.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type (
4 | SearchResult struct {
5 | Title string
6 | URL string
7 | }
8 | )
9 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/next-steps.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/128.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/16.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/256.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/32.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/512.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/64.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "properties" : {
7 | "compression-type" : "gpu-optimized-smallest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/Image.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "universal",
13 | "scale" : "3x"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/LaunchIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "compression-type" : "automatic",
5 | "filename" : "launch-64.png",
6 | "idiom" : "universal",
7 | "scale" : "1x"
8 | },
9 | {
10 | "compression-type" : "automatic",
11 | "filename" : "launch-128.png",
12 | "idiom" : "universal",
13 | "scale" : "2x"
14 | },
15 | {
16 | "compression-type" : "automatic",
17 | "filename" : "launch-192.png",
18 | "idiom" : "universal",
19 | "scale" : "3x"
20 | }
21 | ],
22 | "info" : {
23 | "author" : "xcode",
24 | "version" : 1
25 | },
26 | "properties" : {
27 | "compression-type" : "automatic"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/LaunchIcon.imageset/launch-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/LaunchIcon.imageset/launch-128.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/LaunchIcon.imageset/launch-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/LaunchIcon.imageset/launch-192.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/LaunchIcon.imageset/launch-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/Cherie/Assets.xcassets/LaunchIcon.imageset/launch-64.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Entitlements/.gitignore:
--------------------------------------------------------------------------------
1 | /GoogleService-Info.plist
2 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Entitlements/Entitlements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | production
7 | com.apple.developer.associated-domains
8 |
9 | applinks:cherie.chatbond.app
10 | webcredentials:cherie.chatbond.app
11 |
12 | com.apple.security.app-sandbox
13 |
14 | com.apple.security.files.user-selected.read-write
15 |
16 | com.apple.security.network.client
17 |
18 | com.apple.security.personal-information.location
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/GoogleService-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | API_KEY
6 | AIzaSyA58RRn0Md1IiiiMccoTf4ieCsymSTR8rc
7 | GCM_SENDER_ID
8 | 228226158473
9 | PLIST_VERSION
10 | 1
11 | BUNDLE_ID
12 | app.chatbond.cherie
13 | PROJECT_ID
14 | cherie-407714
15 | STORAGE_BUCKET
16 | cherie-407714.appspot.com
17 | IS_ADS_ENABLED
18 |
19 | IS_ANALYTICS_ENABLED
20 |
21 | IS_APPINVITE_ENABLED
22 |
23 | IS_GCM_ENABLED
24 |
25 | IS_SIGNIN_ENABLED
26 |
27 | GOOGLE_APP_ID
28 | 1:228226158473:ios:3992b9065b37a8fa699b58
29 |
30 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Printer.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import WebKit
3 |
4 | func printView(webView: WKWebView){
5 | let printController = UIPrintInteractionController.shared
6 |
7 | let printInfo = UIPrintInfo(dictionary:nil)
8 | printInfo.outputType = UIPrintInfo.OutputType.general
9 | printInfo.jobName = (webView.url?.absoluteString)!
10 | printInfo.duplex = UIPrintInfo.Duplex.none
11 | printInfo.orientation = UIPrintInfo.Orientation.portrait
12 |
13 | printController.printPageRenderer = UIPrintPageRenderer()
14 |
15 | printController.printPageRenderer?.addPrintFormatter(webView.viewPrintFormatter(), startingAtPageAt: 0)
16 |
17 | printController.printInfo = printInfo
18 | printController.showsNumberOfCopies = true
19 | printController.present(animated: true)
20 | }
21 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Cherie/Settings.swift:
--------------------------------------------------------------------------------
1 | import WebKit
2 |
3 | struct Cookie {
4 | var name: String
5 | var value: String
6 | }
7 |
8 | let gcmMessageIDKey = "00000000000" // update this with actual ID if using Firebase
9 |
10 | // URL for first launch
11 | let rootUrl = URL(string: "https://cherie.chatbond.app")!
12 |
13 | // allowed origin is for what we are sticking to pwa domain
14 | // This should also appear in Info.plist
15 | let allowedOrigins: [String] = ["cherie.chatbond.app"]
16 |
17 | // auth origins will open in modal and show toolbar for back into the main origin.
18 | // These should also appear in Info.plist
19 | let authOrigins: [String] = []
20 | // allowedOrigins + authOrigins <= 10
21 |
22 | let platformCookie = Cookie(name: "app-platform", value: "iOS App Store")
23 |
24 | // UI options
25 | let displayMode = "standalone" // standalone / fullscreen.
26 | let adaptiveUIStyle = true // iOS 15+ only. Change app theme on the fly to dark/light related to WebView background color.
27 | let overrideStatusBar = false // iOS 13-14 only. if you don't support dark/light system theme.
28 | let statusBarTheme = "dark" // dark / light, related to override option.
29 | let pullToRefresh = true // Enable/disable pull down to refresh page
30 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | platform :ios, '15.0'
3 |
4 | target 'Cherie' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 |
8 | # Add the pod for Firebase Cloud Messaging
9 | pod 'Firebase/Messaging'
10 |
11 | end
12 |
13 | post_install do |installer|
14 | installer.pods_project.targets.each do |target|
15 | target.build_configurations.each do |config|
16 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
17 | end
18 | end
19 | end
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/launch-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/launch-128.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/launch-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/launch-192.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/launch-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/launch-256.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/launch-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/launch-512.png
--------------------------------------------------------------------------------
/pwabuilder-ios-wrapper/src/launch-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/pwabuilder-ios-wrapper/src/launch-64.png
--------------------------------------------------------------------------------
/seeder/seeder_test.go:
--------------------------------------------------------------------------------
1 | package seeder_test
2 |
3 | import (
4 | "database/sql"
5 | "testing"
6 |
7 | "github.com/jackc/pgx/stdlib"
8 | "github.com/mikestefanello/pagoda/config"
9 | "github.com/mikestefanello/pagoda/pkg/tests"
10 | "github.com/mikestefanello/pagoda/seeder"
11 | )
12 |
13 | func init() {
14 | // Register "pgx" as "postgres" explicitly for database/sql
15 | sql.Register("postgres", stdlib.GetDefaultDriver())
16 | }
17 |
18 | // TestSeeder tests the seeder code, confirming that it runs.
19 | func TestSeeder(t *testing.T) {
20 | client, _ := tests.CreateTestContainerPostgresEntClient(t)
21 | defer client.Close()
22 |
23 | config := config.Config{}
24 | seeder.RunIdempotentSeeder(&config, client)
25 |
26 | // Assert stuff
27 |
28 | // Seeder should be idempotent and not throw exceptions if re-run
29 | seeder.RunIdempotentSeeder(&config, client)
30 | }
31 |
--------------------------------------------------------------------------------
/static/cherie_pwa_apple-icon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/static/cherie_pwa_apple-icon-180.png
--------------------------------------------------------------------------------
/static/cherie_pwa_manifest-icon-192.maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/static/cherie_pwa_manifest-icon-192.maskable.png
--------------------------------------------------------------------------------
/static/cherie_pwa_manifest-icon-512.maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/static/cherie_pwa_manifest-icon-512.maskable.png
--------------------------------------------------------------------------------
/static/cherie_pwa_manifest-icon-96.maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/static/cherie_pwa_manifest-icon-96.maskable.png
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/static/favicon.png
--------------------------------------------------------------------------------
/static/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/static/icon.png
--------------------------------------------------------------------------------
/static/meta_vanilla_bundle.json:
--------------------------------------------------------------------------------
1 | {"inputs":{"javascript/vanilla/cal_heatmap.js":{"bytes":6362,"imports":[],"format":"esm"},"javascript/vanilla/load_scripts_and_styles.js":{"bytes":1201,"imports":[],"format":"esm"},"javascript/vanilla/main.js":{"bytes":2455,"imports":[{"path":"javascript/vanilla/cal_heatmap.js","kind":"import-statement","original":"./cal_heatmap"},{"path":"javascript/vanilla/load_scripts_and_styles.js","kind":"import-statement","original":"./load_scripts_and_styles"}],"format":"esm"}},"outputs":{"static/vanilla_bundle.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":15804},"static/vanilla_bundle.js":{"imports":[],"exports":[],"entryPoint":"javascript/vanilla/main.js","inputs":{"javascript/vanilla/cal_heatmap.js":{"bytesInOutput":2636},"javascript/vanilla/load_scripts_and_styles.js":{"bytesInOutput":416},"javascript/vanilla/main.js":{"bytesInOutput":1019}},"bytes":4139}}}
--------------------------------------------------------------------------------
/styles/styles.css:
--------------------------------------------------------------------------------
1 | @import './tailwind_components.css';
2 |
3 | @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap');
4 |
5 | @tailwind base;
6 | @tailwind components;
7 | @tailwind utilities;
8 |
9 |
10 | [x-cloak] {
11 | display: none !important;
12 | }
13 |
14 | :root {
15 | --brightness-normal: 1;
16 | /* No change by default */
17 | --brightness-hover-light: 0.15;
18 | /* Lighter in light mode */
19 | --brightness-hover-dark: 0.3;
20 | /* Darker in dark mode */
21 | }
22 |
23 | .hover-brightness {
24 | transition: filter 0.3s ease;
25 | }
26 |
27 | .hover-brightness:hover {
28 | transition: filter 0.3s ease;
29 | background-image: linear-gradient(rgba(0, 0, 0, var(--brightness-hover)), rgba(0, 0, 0, var(--brightness-hover)));
30 | }
31 |
32 | .tiptap .is-empty:first-child:before {
33 | @apply text-slate-300;
34 | @apply content-[attr(data-placeholder)];
35 | @apply float-left;
36 | @apply pointer-events-none;
37 | @apply h-0;
38 | }
--------------------------------------------------------------------------------
/styles/tailwind_components.css:
--------------------------------------------------------------------------------
1 | /* .label {
2 | @apply block text-sm font-medium text-gray-700 dark: text-gray-100;
3 | } */
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./javascript/**/*.{js,svelte}",
4 | "./**/*.templ",
5 | "./node_modules/flowbite/**/*.js",
6 | ],
7 | theme: {
8 | extend: {
9 | backdropBlur: {
10 | xs: "2px",
11 | },
12 | fontFamily: {
13 | PlayfairDisplay: ["Playfair Display", "serif"],
14 | },
15 | },
16 | },
17 | // https://themes.ionevolve.com/
18 | daisyui: {
19 | themes: [
20 | {
21 | lightmode: {
22 | // Change to any existing daisyui theme or make your own
23 | ...require("daisyui/src/theming/themes")["cmyk"],
24 | // Edit styles if required
25 | primary: "white",
26 | secondary: "#DEFBFB",
27 | accent: "#FA6A7D",
28 | neutral: "#919191",
29 | "base-100": "",
30 | info: "#623CEA",
31 | success: "#87FF65",
32 | warning: "#FFC759",
33 | error: "#A30000",
34 | },
35 | },
36 | {
37 | darkmode: {
38 | // Change to any existing daisyui theme or make your own
39 | ...require("daisyui/src/theming/themes")["business"],
40 | // Edit styles if required
41 | primary: "#111827",
42 | secondary: "#222833",
43 | accent: "#FA6A7D",
44 | neutral: "#494949",
45 | "base-100": "#010D14",
46 | info: "#623CEA",
47 | success: "#80D569",
48 | warning: "#FFC759",
49 | error: "#A30000",
50 | },
51 | },
52 | ],
53 | },
54 | plugins: [require("daisyui"), require("flowbite/plugin")],
55 | };
56 |
--------------------------------------------------------------------------------
/templates/components/accordion.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "fmt"
4 |
5 | templ AccordionItem(title string, expanded bool) {
6 |
11 |
15 |
20 |
23 | { children... }
24 |
25 |
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/templates/components/documentation.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ SectionTitle(title string) {
4 | { title }
5 | }
6 |
7 | templ SubSectionTitle(title string) {
8 | { title }
9 | }
10 |
--------------------------------------------------------------------------------
/templates/components/empty_page_msg.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import ()
4 |
5 | templ EmptyPageMessage(message, styleClasses string) {
6 |
10 | { message }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/templates/components/forms.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ FormCSRF(token string) {
4 |
5 | }
6 |
7 | templ FormFieldErrors(errs []string) {
8 | for _, err := range errs {
9 |
14 |
23 | { err }
24 |
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/templates/components/icons.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ TechIcon(styleClasses, websiteUrl, iconUrl, altText string) {
4 |
5 |
11 |
12 | }
13 |
14 | templ TechIconWithDarkAndLightModes(styleClasses, websiteUrl, lightModeIconUrl, darkModeIconUrl, altText string) {
15 |
16 |
22 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/templates/components/loading.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ PageLoadingIndicator() {
4 |
5 |
6 |
10 |
11 |
12 |
25 | }
26 |
27 | templ BottomLoadingIndicator() {
28 |
29 |
30 |
34 |
35 |
36 |
49 | }
50 |
--------------------------------------------------------------------------------
/templates/components/payments.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/pkg/domain"
6 | "github.com/mikestefanello/pagoda/pkg/routing/routenames"
7 | )
8 |
9 | templ ManageSubscriptionButton(page *controller.Page, subscription domain.ProductType, isTrial bool) {
10 | if subscription == domain.ProductTypeFree || (subscription == domain.ProductTypePro && isTrial) {
11 | @PricingPage(page)
12 | } else {
13 | // This is an existing customer
14 | @StripePortal(page)
15 | }
16 | }
17 |
18 | templ PricingPage(page *controller.Page) {
19 |
31 | }
32 |
33 | templ StripePortal(page *controller.Page) {
34 |
44 | }
45 |
--------------------------------------------------------------------------------
/templates/components/prev_nav.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ PrevNavBarWithTitle(prevURL, avatarURL, title string) {
4 | // NOTE: the following is a spacer for mobile view, so that the hamburger icon is not shadowed when at the top of the screen.
5 |
6 |
9 |
10 |
28 |
29 |
30 | if len(avatarURL) != 0 {
31 |
32 |

33 |
34 | }
35 |
36 | { title }
37 |
38 |
39 |
40 |
41 |
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/templates/components/sidebar.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | )
6 |
7 | templ Sidebar(page *controller.Page) {
8 |
21 | }
22 |
--------------------------------------------------------------------------------
/templates/components/sidebar_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.865
4 | package components
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import (
12 | "github.com/mikestefanello/pagoda/pkg/controller"
13 | )
14 |
15 | func Sidebar(page *controller.Page) templ.Component {
16 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
17 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
18 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
19 | return templ_7745c5c3_CtxErr
20 | }
21 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
22 | if !templ_7745c5c3_IsBuffer {
23 | defer func() {
24 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
25 | if templ_7745c5c3_Err == nil {
26 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
27 | }
28 | }()
29 | }
30 | ctx = templ.InitializeContext(ctx)
31 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
32 | if templ_7745c5c3_Var1 == nil {
33 | templ_7745c5c3_Var1 = templ.NopComponent
34 | }
35 | ctx = templ.ClearChildren(ctx)
36 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
45 | if templ_7745c5c3_Err != nil {
46 | return templ_7745c5c3_Err
47 | }
48 | return nil
49 | })
50 | }
51 |
52 | var _ = templruntime.GeneratedTemplate
53 |
--------------------------------------------------------------------------------
/templates/components/theme_toggle.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ ThemeToggle(id string) {
4 |
7 | @initThemeToggle(id)
8 | }
9 |
10 | script initThemeToggle(id string ) {
11 | renderSvelteComponent('ThemeToggle', id, {});
12 | }
13 |
--------------------------------------------------------------------------------
/templates/components/tooltip.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "fmt"
4 |
5 | templ ToolTip(toolTipIndicatorText, bubbleText string) {
6 | { toolTipIndicatorText }
10 | }
11 |
--------------------------------------------------------------------------------
/templates/components/top_banner.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ BonfireBanner() {
4 |
5 |
6 |
12 |
13 |
16 |
17 |
35 | }
36 |
--------------------------------------------------------------------------------
/templates/emails/subscription_confirmation.templ:
--------------------------------------------------------------------------------
1 | package emails
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | )
6 |
7 | templ SubscriptionConfirmation(page *controller.Page) {
8 | }
9 |
--------------------------------------------------------------------------------
/templates/emails/subscription_confirmation_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.865
4 | package emails
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import (
12 | "github.com/mikestefanello/pagoda/pkg/controller"
13 | )
14 |
15 | func SubscriptionConfirmation(page *controller.Page) templ.Component {
16 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
17 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
18 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
19 | return templ_7745c5c3_CtxErr
20 | }
21 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
22 | if !templ_7745c5c3_IsBuffer {
23 | defer func() {
24 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
25 | if templ_7745c5c3_Err == nil {
26 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
27 | }
28 | }()
29 | }
30 | ctx = templ.InitializeContext(ctx)
31 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
32 | if templ_7745c5c3_Var1 == nil {
33 | templ_7745c5c3_Var1 = templ.NopComponent
34 | }
35 | ctx = templ.ClearChildren(ctx)
36 | return nil
37 | })
38 | }
39 |
40 | var _ = templruntime.GeneratedTemplate
41 |
--------------------------------------------------------------------------------
/templates/emails/test.templ:
--------------------------------------------------------------------------------
1 | package emails
2 |
3 | templ TestEmail() {
4 | { "Test email template. See services/mail.go to provide your implementation." }
5 | }
6 |
--------------------------------------------------------------------------------
/templates/emails/test_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.865
4 | package emails
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | func TestEmail() templ.Component {
12 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
13 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
14 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
15 | return templ_7745c5c3_CtxErr
16 | }
17 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
18 | if !templ_7745c5c3_IsBuffer {
19 | defer func() {
20 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
21 | if templ_7745c5c3_Err == nil {
22 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
23 | }
24 | }()
25 | }
26 | ctx = templ.InitializeContext(ctx)
27 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
28 | if templ_7745c5c3_Var1 == nil {
29 | templ_7745c5c3_Var1 = templ.NopComponent
30 | }
31 | ctx = templ.ClearChildren(ctx)
32 | var templ_7745c5c3_Var2 string
33 | templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("Test email template. See services/mail.go to provide your implementation.")
34 | if templ_7745c5c3_Err != nil {
35 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/emails/test.templ`, Line: 4, Col: 81}
36 | }
37 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
38 | if templ_7745c5c3_Err != nil {
39 | return templ_7745c5c3_Err
40 | }
41 | return nil
42 | })
43 | }
44 |
45 | var _ = templruntime.GeneratedTemplate
46 |
--------------------------------------------------------------------------------
/templates/helpers/helpers.templ:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/labstack/gommon/random"
8 | "github.com/mikestefanello/pagoda/config"
9 | "html/template"
10 | "io"
11 | "strings"
12 | )
13 |
14 | var (
15 | // CacheBuster stores a random string used as a cache buster for static files.
16 | CacheBuster = random.String(10)
17 | )
18 |
19 | // File appends a cache buster to a given filepath so it can remain cached until the app is restarted
20 | func ServiceWorkerFile(filepath string) string {
21 | return fmt.Sprintf("/%s?v=%s", filepath, CacheBuster)
22 | }
23 |
24 | // File appends a cache buster to a given filepath so it can remain cached until the app is restarted
25 | func File(filepath string) string {
26 | return fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, filepath, CacheBuster)
27 | }
28 |
29 | // Link outputs HTML for a link element, providing the ability to dynamically set the active class
30 | templ Link(url, text, currentPath string, classes ...string) {
31 | { text }
32 | }
33 |
34 | func isEqualValue(item, expected, val string) string {
35 | if item == expected {
36 | return val
37 | }
38 | return ""
39 | }
40 |
41 | func UnsafeHTML(s template.HTML) templ.Component {
42 | return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
43 | io.WriteString(w, string(s))
44 | return nil
45 | })
46 | }
47 |
48 | type Fn string
49 |
50 | func ToJSON(v any) string {
51 | b, _ := json.Marshal(v)
52 | return string(b)
53 | }
54 |
55 | func ToJS(data any, functions map[string]Fn) string {
56 | var pairs []string
57 | for k, v := range toMap(data) {
58 | pairs = append(pairs, fmt.Sprintf("%s: %s", jsonString(k), jsonString(v)))
59 | }
60 | for k, fn := range functions {
61 | pairs = append(pairs, fmt.Sprintf("%s: %s", jsonString(k), fn))
62 | }
63 | return "{" + strings.Join(pairs, ",") + "}"
64 | }
65 |
66 | func toMap(data any) (m map[string]any) {
67 | jb, _ := json.Marshal(data)
68 | json.Unmarshal(jb, &m)
69 | return m
70 | }
71 |
72 | func jsonString(s any) string {
73 | ss, _ := json.Marshal(s)
74 | return string(ss)
75 | }
76 |
--------------------------------------------------------------------------------
/templates/layouts/auth.templ:
--------------------------------------------------------------------------------
1 | package layouts
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/templates/components"
6 | "github.com/mikestefanello/pagoda/pkg/routing/routenames"
7 | )
8 |
9 | templ Auth(content templ.Component, page *controller.Page) {
10 |
11 |
12 |
13 | @components.Metatags(page)
14 | @components.CSS()
15 | @components.JS()
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | @components.Navbar(page)
24 |
25 |
30 |
40 |
43 | if len(page.Title) > 0 {
44 |
{ page.Title }
45 | }
46 |
47 | @components.Messages(page)
48 | @content
49 |
50 |
51 |
52 | @components.PageLoadingIndicator()
53 | // TODO: Links not working when using here
54 | // @components.TextFooter(page)
55 | @components.JSFooter(page)
56 |
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/templates/layouts/documentation.templ:
--------------------------------------------------------------------------------
1 | package layouts
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/templates/components"
6 | )
7 |
8 | templ Documentation(content templ.Component, page *controller.Page) {
9 |
10 |
11 |
12 | @components.Metatags(page)
13 | @components.CSS()
14 | @components.JS()
15 |
16 |
17 |
18 | @components.Drawer(page, true)
19 |
20 |
21 | @components.Navbar(page)
22 |
23 |
24 |
25 | @components.Drawer(page, false)
26 |
27 |
31 | @content
32 |
33 |
34 | @components.PageLoadingIndicator()
35 | // TODO: links not working when using here
36 | // @components.TextFooter(page)
37 | @components.JSFooter(page)
38 |
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/templates/layouts/email.templ:
--------------------------------------------------------------------------------
1 | package layouts
2 |
3 | templ Email(content templ.Component) {
4 | @content
5 | }
6 |
--------------------------------------------------------------------------------
/templates/layouts/email_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.865
4 | package layouts
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | func Email(content templ.Component) templ.Component {
12 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
13 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
14 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
15 | return templ_7745c5c3_CtxErr
16 | }
17 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
18 | if !templ_7745c5c3_IsBuffer {
19 | defer func() {
20 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
21 | if templ_7745c5c3_Err == nil {
22 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
23 | }
24 | }()
25 | }
26 | ctx = templ.InitializeContext(ctx)
27 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
28 | if templ_7745c5c3_Var1 == nil {
29 | templ_7745c5c3_Var1 = templ.NopComponent
30 | }
31 | ctx = templ.ClearChildren(ctx)
32 | templ_7745c5c3_Err = content.Render(ctx, templ_7745c5c3_Buffer)
33 | if templ_7745c5c3_Err != nil {
34 | return templ_7745c5c3_Err
35 | }
36 | return nil
37 | })
38 | }
39 |
40 | var _ = templruntime.GeneratedTemplate
41 |
--------------------------------------------------------------------------------
/templates/layouts/landing_page.templ:
--------------------------------------------------------------------------------
1 | package layouts
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/templates/components"
6 | )
7 |
8 | templ LandingPage(content templ.Component, page *controller.Page) {
9 |
10 |
11 |
12 | @components.Metatags(page)
13 | @components.CSS()
14 | @components.JS()
15 |
16 |
17 | // @components.BonfireBanner()
18 |
19 |
20 | @components.Navbar(page)
21 |
22 |
23 | // @components.PWAMobileInstallButton(page)
24 |
25 |
30 | @content
31 |
32 |
33 | @components.PageLoadingIndicator()
34 | // TODO: links not working when using here
35 | // @components.TextFooter(page)
36 | @components.JSFooter(page)
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/templates/layouts/main.templ:
--------------------------------------------------------------------------------
1 | package layouts
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/templates/components"
6 | )
7 |
8 | templ Main(content templ.Component, page *controller.Page) {
9 |
10 |
11 |
12 | @components.Metatags(page)
13 | @components.CSS()
14 | @components.JS()
15 |
16 |
17 | // @components.BonfireBanner()
18 |
19 |
20 |
21 | // @components.Drawer(page)
22 |
23 |
24 |
25 | @components.Navbar(page)
26 |
27 |
28 | // @components.PWAMobileInstallButton(page)
29 |
32 | // NOTE: to get below working, add "lg:ml-64" to the parent div
33 | //
34 | // @components.Sidebar(page)
35 | //
36 |
37 |
41 |
42 |
43 | if len(page.Title) > 0 {
44 |
{ page.Title }
45 | }
46 | @components.Messages(page)
47 | @content
48 |
49 | if page.ShowBottomNavbar {
50 |
51 |
52 | @components.BottomNav(page)
53 |
54 | }
55 |
56 |
57 |
58 | @components.PageLoadingIndicator()
59 | // @components.TextFooter(page)
60 | @components.JSFooter(page)
61 |
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/templates/pages/about.templ:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/pkg/types"
6 | "github.com/mikestefanello/pagoda/templates/components"
7 | )
8 |
9 | templ About(page *controller.Page) {
10 | @components.PrevNavBarWithTitle("", "", "☎️ Contact us")
11 | if data, ok := page.Data.(types.AboutData); ok {
12 | @aboutUsData(data)
13 | }
14 | }
15 |
16 | templ aboutUsData(data types.AboutData) {
17 |
18 |
19 |
20 | { "We're dedicated to improving our app and we'd love to hear from you!" }
21 |
22 |
23 | { "If you have any bug reports, feature requests, or suggestions, please reach out to us. 🙏😃❤️" }
24 |
25 |
26 | { "You can send us an email by clicking on the following:" }
27 |
28 |
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/templates/pages/docs_architecture.templ:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | // "github.com/mikestefanello/pagoda/templates/components"
6 | templ DocumentationArchitecturePage(page *controller.Page) {
7 | @docsPageLayout(page) {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/templates/pages/docs_architecture_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.865
4 | package pages
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import "github.com/mikestefanello/pagoda/pkg/controller"
12 |
13 | // "github.com/mikestefanello/pagoda/templates/components"
14 | func DocumentationArchitecturePage(page *controller.Page) templ.Component {
15 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
16 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
17 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
18 | return templ_7745c5c3_CtxErr
19 | }
20 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
21 | if !templ_7745c5c3_IsBuffer {
22 | defer func() {
23 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
24 | if templ_7745c5c3_Err == nil {
25 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
26 | }
27 | }()
28 | }
29 | ctx = templ.InitializeContext(ctx)
30 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
31 | if templ_7745c5c3_Var1 == nil {
32 | templ_7745c5c3_Var1 = templ.NopComponent
33 | }
34 | ctx = templ.ClearChildren(ctx)
35 | templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
36 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
37 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
38 | if !templ_7745c5c3_IsBuffer {
39 | defer func() {
40 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
41 | if templ_7745c5c3_Err == nil {
42 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
43 | }
44 | }()
45 | }
46 | ctx = templ.InitializeContext(ctx)
47 | return nil
48 | })
49 | templ_7745c5c3_Err = docsPageLayout(page).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
50 | if templ_7745c5c3_Err != nil {
51 | return templ_7745c5c3_Err
52 | }
53 | return nil
54 | })
55 | }
56 |
57 | var _ = templruntime.GeneratedTemplate
58 |
--------------------------------------------------------------------------------
/templates/pages/docs_emails.templ:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | // "github.com/mikestefanello/pagoda/templates/components"
6 | templ DocumentationEmailingPage(page *controller.Page) {
7 | @docsPageLayout(page) {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/templates/pages/docs_emails_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.865
4 | package pages
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import "github.com/mikestefanello/pagoda/pkg/controller"
12 |
13 | // "github.com/mikestefanello/pagoda/templates/components"
14 | func DocumentationEmailingPage(page *controller.Page) templ.Component {
15 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
16 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
17 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
18 | return templ_7745c5c3_CtxErr
19 | }
20 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
21 | if !templ_7745c5c3_IsBuffer {
22 | defer func() {
23 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
24 | if templ_7745c5c3_Err == nil {
25 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
26 | }
27 | }()
28 | }
29 | ctx = templ.InitializeContext(ctx)
30 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
31 | if templ_7745c5c3_Var1 == nil {
32 | templ_7745c5c3_Var1 = templ.NopComponent
33 | }
34 | ctx = templ.ClearChildren(ctx)
35 | templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
36 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
37 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
38 | if !templ_7745c5c3_IsBuffer {
39 | defer func() {
40 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
41 | if templ_7745c5c3_Err == nil {
42 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
43 | }
44 | }()
45 | }
46 | ctx = templ.InitializeContext(ctx)
47 | return nil
48 | })
49 | templ_7745c5c3_Err = docsPageLayout(page).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
50 | if templ_7745c5c3_Err != nil {
51 | return templ_7745c5c3_Err
52 | }
53 | return nil
54 | })
55 | }
56 |
57 | var _ = templruntime.GeneratedTemplate
58 |
--------------------------------------------------------------------------------
/templates/pages/error.templ:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "fmt"
6 | "github.com/mikestefanello/pagoda/pkg/routing/routenames"
7 | )
8 |
9 | templ Error(page *controller.Page) {
10 |
11 |
12 |
13 |
16 |
17 | @title(page)
18 |
19 |
20 | @subtitle(page)
21 |
22 |
Back to Homepage
31 |
32 |
33 |
34 | }
35 |
36 | templ title(page *controller.Page) {
37 | if page.StatusCode >= 500 {
38 | { "Please try again." }
39 | } else if page.StatusCode == 403 || page.StatusCode == 401 {
40 | { "You are not authorized to view the requested page." }
41 | } else if page.StatusCode == 404 {
42 | { "Something's missing." }
43 | } else {
44 | { "Something went wrong" }
45 | }
46 | }
47 |
48 | templ subtitle(page *controller.Page) {
49 | if page.StatusCode >= 500 {
50 | { "Oops! Something went wrong on our end. Please refresh the page or try again later." }
51 | } else if page.StatusCode == 403 || page.StatusCode == 401 {
52 | { "You are not authorized to view the requested page." }
53 | } else if page.StatusCode == 404 {
54 | { "Sorry, we can't find that page. You'll find lots to explore on the home page. " }
55 | } else {
56 | { "Something went wrong. Please refresh the page or try again later." }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/templates/pages/forgot_password.templ:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/pkg/types"
6 | "github.com/mikestefanello/pagoda/templates/components"
7 | "github.com/mikestefanello/pagoda/pkg/routing/routenames"
8 | )
9 |
10 | templ ForgotPassword(page *controller.Page) {
11 | if form, ok := page.Form.(*types.ForgotPasswordForm); ok {
12 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/templates/pages/healthcheck.templ:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import "github.com/mikestefanello/pagoda/pkg/controller"
4 |
5 | templ HealthCheck(page *controller.Page) {
6 |
7 |
Service is up and running!
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/templates/pages/healthcheck_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.865
4 | package pages
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import "github.com/mikestefanello/pagoda/pkg/controller"
12 |
13 | func HealthCheck(page *controller.Page) templ.Component {
14 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
15 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
16 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
17 | return templ_7745c5c3_CtxErr
18 | }
19 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
20 | if !templ_7745c5c3_IsBuffer {
21 | defer func() {
22 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
23 | if templ_7745c5c3_Err == nil {
24 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
25 | }
26 | }()
27 | }
28 | ctx = templ.InitializeContext(ctx)
29 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
30 | if templ_7745c5c3_Var1 == nil {
31 | templ_7745c5c3_Var1 = templ.NopComponent
32 | }
33 | ctx = templ.ClearChildren(ctx)
34 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Service is up and running!
")
35 | if templ_7745c5c3_Err != nil {
36 | return templ_7745c5c3_Err
37 | }
38 | return nil
39 | })
40 | }
41 |
42 | var _ = templruntime.GeneratedTemplate
43 |
--------------------------------------------------------------------------------
/templates/pages/profile.templ:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/pkg/types"
6 | "github.com/mikestefanello/pagoda/templates/components"
7 | )
8 |
9 | templ ProfilePage(page *controller.Page) {
10 | if data, ok := page.Data.(types.ProfilePageData); ok {
11 | @components.PrevNavBarWithTitle(page.ToURL("convo", data.Profile.ID), "", "❤️ Your Partner")
12 | @components.Profile(page, data.Profile, data.IsSelf, false, data.UploadGalleryPicUrl, data.UploadProfilePicUrl, data.GalleryPicsMaxCount)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/templates/pages/profile_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.865
4 | package pages
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import (
12 | "github.com/mikestefanello/pagoda/pkg/controller"
13 | "github.com/mikestefanello/pagoda/pkg/types"
14 | "github.com/mikestefanello/pagoda/templates/components"
15 | )
16 |
17 | func ProfilePage(page *controller.Page) templ.Component {
18 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
19 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
20 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
21 | return templ_7745c5c3_CtxErr
22 | }
23 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
24 | if !templ_7745c5c3_IsBuffer {
25 | defer func() {
26 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
27 | if templ_7745c5c3_Err == nil {
28 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
29 | }
30 | }()
31 | }
32 | ctx = templ.InitializeContext(ctx)
33 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
34 | if templ_7745c5c3_Var1 == nil {
35 | templ_7745c5c3_Var1 = templ.NopComponent
36 | }
37 | ctx = templ.ClearChildren(ctx)
38 | if data, ok := page.Data.(types.ProfilePageData); ok {
39 | templ_7745c5c3_Err = components.PrevNavBarWithTitle(page.ToURL("convo", data.Profile.ID), "", "❤️ Your Partner").Render(ctx, templ_7745c5c3_Buffer)
40 | if templ_7745c5c3_Err != nil {
41 | return templ_7745c5c3_Err
42 | }
43 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " ")
44 | if templ_7745c5c3_Err != nil {
45 | return templ_7745c5c3_Err
46 | }
47 | templ_7745c5c3_Err = components.Profile(page, data.Profile, data.IsSelf, false, data.UploadGalleryPicUrl, data.UploadProfilePicUrl, data.GalleryPicsMaxCount).Render(ctx, templ_7745c5c3_Buffer)
48 | if templ_7745c5c3_Err != nil {
49 | return templ_7745c5c3_Err
50 | }
51 | }
52 | return nil
53 | })
54 | }
55 |
56 | var _ = templruntime.GeneratedTemplate
57 |
--------------------------------------------------------------------------------
/templates/pages/reset_password.templ:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "github.com/mikestefanello/pagoda/pkg/controller"
5 | "github.com/mikestefanello/pagoda/pkg/types"
6 | "github.com/mikestefanello/pagoda/templates/components"
7 | )
8 |
9 | templ ResetPassword(page *controller.Page) {
10 | if form, ok := page.Form.(*types.ResetPasswordForm); ok {
11 |
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/templates/templates.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | type (
4 | Page string
5 | )
6 |
7 | const (
8 | PageAbout Page = "about"
9 | PageLanding Page = "landing"
10 | PageContact Page = "contact"
11 | PageError Page = "error"
12 | PageForgotPassword Page = "forgot-password"
13 | PageHome Page = "home"
14 | PageLogin Page = "login"
15 | PageRegister Page = "register"
16 | PageResetPassword Page = "reset-password"
17 | PageEmailSubscribe Page = "email-subscribe"
18 | PagePreferences Page = "preferences"
19 | PagePhoneNumber Page = "preferences.phone"
20 | PageDisplayName Page = "preferences.display_name"
21 | PageHomeFeed Page = "home_feed"
22 | PageInstallApp Page = "install_app"
23 | PageProfile Page = "profile"
24 | PageNotifications Page = "notifications"
25 | PageHealthcheck Page = "healthcheck"
26 | PagePricing Page = "pricing"
27 | PageSuccessfullySubscribed Page = "successfully_subscribed"
28 | PageDeleteAccount Page = "delete_account.page"
29 | PagePrivacyPolicy Page = "privacy_policy"
30 | PageWiki Page = "wiki"
31 |
32 | SSEAnsweredByFriend Page = "sse_answered_by_friend"
33 | )
34 |
--------------------------------------------------------------------------------
/testdata/photos/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/testdata/photos/1.jpg
--------------------------------------------------------------------------------
/testdata/photos/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leomorpho/goship/0df3e11a307b430f33275dc560ab3d4db782d3f9/testdata/photos/2.jpg
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017", // Specify ECMAScript target version
4 | "module": "esnext", // Specify module code generation
5 | "strict": true, // Enable all strict type-checking options
6 | "esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules
7 | "skipLibCheck": true, // Skip type checking of all declaration files (*.d.ts)
8 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file.
9 | "baseUrl": ".", // Base directory to resolve non-relative module names
10 | "paths": {
11 | "*": ["node_modules/*", "src/types/*"] // Specify paths for modules
12 | },
13 | "allowJs": true, // Allow JavaScript files to be compiled.
14 | "outDir": "./dist", // Redirect output structure to the directory
15 | "rootDir": "./javascript", // Specify the root directory of input files
16 | "moduleResolution": "node", // Specify module resolution strategy
17 | "resolveJsonModule": true, // Include modules imported with '.json' extension
18 | "sourceMap": true // Generates corresponding '.map' file
19 | },
20 | "include": ["./javascript/**/*"], // Include all files in the javascript directory
21 | "exclude": ["node_modules", "**/*.spec.ts"] // Exclude node_modules and test files
22 | }
23 |
--------------------------------------------------------------------------------