Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Internal identifiers (Go module `schej.it/server`, Mongo DB `schej-it`, prod ema
- MongoDB backup/restore: `mongodump --host=localhost:27017 --db=schej-it` / `mongorestore --uri mongodb://localhost:27017 ./dump --drop`.

### Required env vars for local server boot
`SESSION_SECRET` (≥32 chars) is enforced at startup. `CLIENT_ID`/`CLIENT_SECRET` (Google OAuth) and `ENCRYPTION_KEY` are required for most flows. See `server/.env.template` and `DEPLOYMENT.md` for the full list (Stripe, Microsoft, Listmonk, Slack, Discord, Gmail, etc.).
`SESSION_SECRET` (≥32 chars) is enforced at startup. `CLIENT_ID`/`CLIENT_SECRET` (Google OAuth) and `ENCRYPTION_KEY` are required for most flows. See `server/.env.template` and `DEPLOYMENT.md` for the full list (Microsoft, Listmonk, Slack, Discord, Gmail, etc.).

For local frontend → local backend, set `CORS_ORIGINS=http://localhost:8080` in `server/.env`.

Expand All @@ -40,7 +40,7 @@ For local frontend → local backend, set `CORS_ORIGINS=http://localhost:8080` i
### Backend (Gin + MongoDB)
`server/main.go` wires everything: CORS, cookie sessions, Mongo init (`db.Init`), Google Cloud Tasks init (`services/gcloud.InitTasks`), then mounts API groups under `/api` via `routes.Init*` and `slackbot.InitSlackbot`. After API routes, it walks `frontend/dist` and registers each file as a static route, loads `index.html` as a template, and falls back to a `NoRoute` handler that injects per-route OG meta tags (e.g. for `/e/:eventId` it looks up the event to set the title and OG image).

- `routes/` — HTTP handlers grouped by domain: `auth.go`, `user.go`, `users.go`, `events.go`, `folders.go`, `analytics.go`, `stripe.go`. Route comments use Swag annotations; `swag init` regenerates `docs/`.
- `routes/` — HTTP handlers grouped by domain: `auth.go`, `user.go`, `users.go`, `events.go`, `folders.go`, `analytics.go`. Route comments use Swag annotations; `swag init` regenerates `docs/`.
- `models/` — Mongo document structs (`Event`, `User`, `Response`, `Folder`, `Attendee`, `Calendar`, `Set`, `Otp`, `FriendRequest`, `Location`, `DailyUserLog`).
- `db/` — Mongo accessors per model (`events.go`, `users.go`, `folders.go`, `analytics.go`, `utils.go`) plus `init.go`. Treat this as the only layer that talks to Mongo.
- `services/` — external integrations. Notable: `calendar/` (Google, Outlook/Graph, Apple CalDAV via `jonyTF/go-webdav`, generic ICS), `auth/`, `contacts/`, `gcloud/` (Cloud Tasks for scheduled jobs), `listmonk/`, `microsoftgraph/`.
Expand All @@ -51,7 +51,7 @@ For local frontend → local backend, set `CORS_ORIGINS=http://localhost:8080` i
- `logger/` — wraps log file (`logs.log`) + stdout via `gin.DefaultWriter`.

### Frontend (Vue 2 SPA)
- `src/router/index.js` — routes (`Landing`, `Home`, `Event`, `Group`, `Friends`, `Settings`, `SignIn`/`SignUp`/`Auth`, `StripeRedirect`, etc. — see `src/views/`).
- `src/router/index.js` — routes (`Landing`, `Home`, `Event`, `Group`, `Friends`, `Settings`, `SignIn`/`SignUp`/`Auth`, etc. — see `src/views/`).
- `src/store/index.js` — single Vuex store (auth user, events, snackbar, dialogs).
- `src/components/` — organized by feature folder (`event/`, `groups/`, `home/`, `landing/`, `pricing/`, `settings/`, `schedule_overlap/`, `calendar_permission_dialogs/`, `sign_up_form/`, `general/`) plus top-level shared components.
- `src/utils/` — date math (`date_utils.js`, uses `dayjs`/`moment`/`spacetime`), `fetch_utils.js` (API client), `plugin_utils.js` (handles the postMessage plugin API — see `PLUGIN_API_README.md`), `sign_in_utils.js`, `location_utils.js`, `services/` (calendar-provider abstractions on the client side).
Expand Down
21 changes: 0 additions & 21 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,27 +106,6 @@ Create `server/.env` from the template (`server/.env.template`).
| `ENCRYPTION_KEY` | Key for encrypting sensitive data (generate with `openssl rand -base64 32`) |
| `SESSION_SECRET` | Session cookie encryption key (generate with `openssl rand -base64 32`) |

#### Optional — Payments

New purchases go through [Polar](https://polar.sh). Stripe is only kept for subscribers who signed up before the move, whose subscriptions still renew on Stripe.

| Variable | Description |
| ----------------------- | ------------------------------------------------------------------ |
| `POLAR_ACCESS_TOKEN` | Polar Organization Access Token |
| `POLAR_WEBHOOK_SECRET` | Polar webhook endpoint secret |
| `POLAR_SERVER` | `sandbox` or `production` (default: `production`) |
| `POLAR_*_PRODUCT_ID` | Polar product IDs for each plan (monthly, yearly, and student variants; lifetime is disabled) |
| `STRIPE_API_KEY` | Stripe API key (legacy subscribers only) |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret (legacy subscribers only) |

Polar setup:

1. Create one product per plan. Each Polar product has a single pricing model, so monthly and yearly are separate products.
2. Create an Organization Access Token with the `checkouts:write`, `products:read`, and `customer_sessions:write` scopes.
3. Add a webhook endpoint at `https://<your-domain>/api/polar/webhook` with format **Raw** and API version **2026-10**, subscribed to `order.paid`, `subscription.active`, `subscription.past_due`, `subscription.revoked`, and `subscription.migrated`.

For local development, `polar listen http://localhost:3002/api/polar/webhook` forwards webhooks and prints the secret to use for `POLAR_WEBHOOK_SECRET`.

#### Optional — Additional Calendars

| Variable | Description |
Expand Down
11 changes: 0 additions & 11 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"@gtm-support/vue2-gtm": "^2.0.0",
"@rive-app/canvas": "^2.10.0",
"bson-objectid": "^2.0.4",
"canvas-confetti": "^1.9.3",
"color": "^4.2.3",
"copy-image-clipboard": "^2.1.2",
"core-js": "^3.35.1",
Expand Down
1 change: 0 additions & 1 deletion frontend/public/ads.txt

This file was deleted.

1 change: 0 additions & 1 deletion frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
})(window, document, "script", "dataLayer", "GTM-M677X6V")
</script>
<!-- End Google Tag Manager -->
<script async src="https://cdn.fuseplatform.net/publift/tags/2/4241/fuse.js"></script>

<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
Expand Down
54 changes: 8 additions & 46 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@
:no-tabs="newDialogOptions.eventOnly"
:folder-id="newDialogOptions.folderId"
/>
<UpgradeDialog
:value="upgradeDialogVisible"
@input="handleUpgradeDialogInput"
/>
<UpvoteRedditSnackbar />
<div
v-if="showHeader"
Expand All @@ -34,10 +30,10 @@
</router-link>
<v-expand-x-transition>
<span
v-if="isPremiumUser"
v-if="authUser?.hasPaid"
class="tw-ml-2 tw-cursor-default tw-rounded-md tw-bg-[linear-gradient(-25deg,#0a483d,#00994c,#126045,#0a483d)] tw-px-2 tw-py-1 tw-text-sm tw-font-semibold tw-text-white tw-opacity-80"
>
Premium
Supporter
</span>
</v-expand-x-transition>

Expand All @@ -61,14 +57,14 @@
>
Give feedback
</v-btn>
<!-- <v-btn
<v-btn
v-if="!isPhone"
text
href="https://www.paypal.com/donate/?hosted_button_id=KWCH6LGJCP6E6"
target="_blank"
>
Donate
</v-btn> -->
</v-btn>
<v-btn
v-if="$route.name === 'home' && !isPhone"
color="primary"
Expand Down Expand Up @@ -229,31 +225,23 @@ html {
</style>

<script>
import { mapMutations, mapState, mapActions, mapGetters } from "vuex"
import { mapMutations, mapState, mapActions } from "vuex"
import {
get,
getLocation,
isPhone,
post,
signInGoogle,
signInOutlook,
isPremiumUser,
} from "@/utils"
import {
authTypes,
calendarTypes,
eventTypes,
numFreeEvents,
upgradeDialogTypes,
} from "@/constants"
import { authTypes, calendarTypes, eventTypes } from "@/constants"
import AutoSnackbar from "@/components/AutoSnackbar"
import AuthUserMenu from "@/components/AuthUserMenu.vue"
import SignInNotSupportedDialog from "@/components/SignInNotSupportedDialog.vue"
import UpvoteRedditSnackbar from "@/components/UpvoteRedditSnackbar.vue"
import Logo from "@/components/Logo.vue"
import isWebview from "is-ua-webview"
import NewDialog from "./components/NewDialog.vue"
import UpgradeDialog from "@/components/pricing/UpgradeDialog.vue"
import SignInDialog from "@/components/SignInDialog.vue"
import DiscordBanner from "@/components/DiscordBanner.vue"

Expand All @@ -273,7 +261,6 @@ export default {
NewDialog,
UpvoteRedditSnackbar,
Logo,
UpgradeDialog,
SignInDialog,
DiscordBanner,
},
Expand All @@ -287,15 +274,7 @@ export default {
}),

computed: {
...mapGetters(["isPremiumUser"]),
...mapState([
"authUser",
"error",
"info",
"enablePaywall",
"upgradeDialogVisible",
"newDialogOptions",
]),
...mapState(["authUser", "error", "info", "newDialogOptions"]),
isPhone() {
return isPhone(this.$vuetify)
},
Expand Down Expand Up @@ -328,16 +307,9 @@ export default {
...mapMutations([
"setAuthUser",
"setSignUpFormEnabled",
"setPricingPageConversion",
"setEnablePaywall",
"setFeatureFlagsLoaded",
]),
...mapActions([
"getEvents",
"showUpgradeDialog",
"hideUpgradeDialog",
"createNew",
]),
...mapActions(["getEvents", "createNew"]),
handleScroll(e) {
this.scrollY = window.scrollY
},
Expand Down Expand Up @@ -409,21 +381,11 @@ export default {
if (!this.$posthog) return

// this.setSignUpFormEnabled(this.$posthog.isFeatureEnabled("sign-up-form"))
// this.setPricingPageConversion(
// this.$posthog.getFeatureFlag("pricing-page-conversion")
// )
// )
// this.setEnablePaywall(this.$posthog.isFeatureEnabled("enable-paywall"))
this.setFeatureFlagsLoaded(true)
},
trackFeedbackClick() {
this.$posthog.capture("give_feedback_button_clicked")
},
handleUpgradeDialogInput(value) {
if (!value) {
this.hideUpgradeDialog()
}
},
},

async created() {
Expand Down
Binary file removed frontend/src/assets/ads/tomotime.png
Binary file not shown.
Binary file removed frontend/src/assets/ads/tomotime_mobile.png
Binary file not shown.
4 changes: 1 addition & 3 deletions frontend/src/components/AuthUserMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
<v-menu v-if="authUser" offset-y left>
<template v-slot:activator="{ on }">
<v-btn id="user-menu-btn" icon :width="size" :height="size" v-on="on">
<v-avatar :size="size">
<UserAvatarContent :user="authUser" :size="size" />
</v-avatar>
<UserAvatarContent :user="authUser" :size="size" />
</v-btn>
</template>
<v-list class="py-0" :dense="isPhone">
Expand Down
35 changes: 0 additions & 35 deletions frontend/src/components/EventType.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,6 @@
>
<div class="tw-flex tw-flex-col">
{{ eventType.header }}
<div
v-if="
eventType.header === 'Events I created' &&
enablePaywall &&
!isPremiumUser
"
class="tw-flex tw-items-baseline tw-gap-2 tw-text-sm tw-font-normal tw-text-very-dark-gray"
>
<div>
{{ authUser?.numEventsCreated }} / {{ numFreeEvents }} free events
created
</div>
<div
class="tw-cursor-pointer tw-select-none tw-text-xs tw-font-medium tw-text-green tw-underline"
@click="openUpgradeDialog"
>
Upgrade
</div>
</div>
</div>
<v-btn
v-if="eventType.header === 'Events I created'"
Expand Down Expand Up @@ -96,9 +77,6 @@
<script>
import EventItem from "@/components/EventItem.vue"
import FeatureNotReadyDialog from "@/components/FeatureNotReadyDialog.vue"
import { numFreeEvents, upgradeDialogTypes } from "@/constants"
import { mapState, mapActions } from "vuex"
import { isPremiumUser } from "@/utils"

export default {
name: "EventType",
Expand All @@ -121,7 +99,6 @@ export default {
},

computed: {
...mapState(["authUser", "enablePaywall"]),
defaultNumEventsToShow() {
return this.$vuetify.breakpoint.lgAndUp ? 6 : 4
},
Expand All @@ -134,24 +111,12 @@ export default {
// Events are sorted serverside, so no need to sort here
return this.eventType.events
},
numFreeEvents() {
return numFreeEvents
},
isPremiumUser() {
return isPremiumUser(this.authUser)
},
},

methods: {
...mapActions(["showUpgradeDialog"]),
toggleShowAll() {
this.showAll = !this.showAll
},
openUpgradeDialog() {
this.showUpgradeDialog({
type: upgradeDialogTypes.UPGRADE_MANUALLY,
})
},
createFolder() {
this.showFeatureNotReadyDialog = true
this.$posthog?.capture("create_folder_clicked")
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/components/Footer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@
v-bind="attrs"
v-on="on"
>
Support
Contact us
</span>
</template>
<v-card class="tw-p-3">
<div class="tw-text-sm">
Email support at
Email us at
<span class="tw-text-green tw-underline"
>contact@timeful.app</span
>
Expand All @@ -111,10 +111,23 @@
>Give Feedback</a
>
</div>
<div>
<a
href="https://www.paypal.com/donate/?hosted_button_id=KWCH6LGJCP6E6"
target="_blank"
class="tw-text-sm"
>Donate</a
>
</div>
</div>
<!-- Articles -->
<div class="tw-flex tw-flex-col tw-gap-2">
<div class="tw-mb-1 tw-font-bold">Articles</div>
<div>
<a href="/blog/timeful-is-now-free/" class="tw-text-sm"
>Timeful is now completely free</a
>
</div>
<div>
<a href="/blog/schej-is-now-timeful/" class="tw-text-sm"
>Schej is now Timeful</a
Expand Down
22 changes: 0 additions & 22 deletions frontend/src/components/FormerlyKnownAs.vue

This file was deleted.

Loading
Loading