├── .dockerignore ├── .env.development ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ ├── ---feature-request.md │ └── 🛠️-self-hosting-issue.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ ├── pnpm-install │ │ └── action.yml │ └── setup-node │ │ └── action.yml └── workflows │ ├── ci.yml │ ├── docker-image-manual.yml │ ├── docker-image-version-release.yml │ ├── house-keeping.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode └── settings.json ├── .windsurfrules ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apps ├── docs │ ├── README.md │ ├── administrators │ │ ├── deleting-a-meeting-poll.mdx │ │ └── editing-a-meeting-poll.mdx │ ├── contact │ │ └── support.mdx │ ├── contribute │ │ ├── documentation.mdx │ │ ├── donations.mdx │ │ ├── introduction.mdx │ │ └── translations.mdx │ ├── faq.mdx │ ├── favicon.png │ ├── guide │ │ └── participant-guide.mdx │ ├── images │ │ ├── contribute │ │ │ ├── crowdin-project.png │ │ │ ├── edit-documentation.png │ │ │ └── icu-message-format.png │ │ ├── guide │ │ │ ├── voting-in-progress.png │ │ │ ├── voting-submitted.png │ │ │ └── voting-submitting.png │ │ ├── home.jpeg │ │ ├── self-hosting │ │ │ ├── cloudron-logo.svg │ │ │ ├── control-panel.jpeg │ │ │ └── run-on-pikapods.svg │ │ ├── unused │ │ │ ├── polls-page.png │ │ │ ├── response │ │ │ │ ├── editing-response-1.png │ │ │ │ ├── editing-response-2.png │ │ │ │ ├── editing-response.png │ │ │ │ ├── submitting-response-1.png │ │ │ │ └── submitting-response-2.png │ │ │ └── support.png │ │ └── workflow │ │ │ ├── finalize.png │ │ │ ├── finalized.png │ │ │ ├── invite-link.png │ │ │ ├── month-view.png │ │ │ ├── new-poll-page.png │ │ │ ├── review.png │ │ │ ├── voting.png │ │ │ └── week-view.png │ ├── introduction.mdx │ ├── logo │ │ ├── dark.svg │ │ └── light.svg │ ├── mint.json │ ├── package.json │ ├── self-hosting │ │ ├── configuration.mdx │ │ ├── control-panel.mdx │ │ ├── installation │ │ │ └── docker.mdx │ │ ├── introduction.mdx │ │ ├── licensing.mdx │ │ ├── managed-hosting.mdx │ │ ├── pricing.mdx │ │ └── single-sign-on.mdx │ └── workflow │ │ ├── create.mdx │ │ ├── finalize.mdx │ │ └── invite.mdx ├── landing │ ├── .gitignore │ ├── declarations │ │ ├── environment.d.ts │ │ └── i18next.d.ts │ ├── i18next-scanner.config.js │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── .well-known │ │ │ └── microsoft-identity-association.json │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-114x114.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-144x144.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-167x167.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-57x57.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-72x72.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── bimi-logo.svg │ │ ├── favicon-128x128.png │ │ ├── favicon-16x16.png │ │ ├── favicon-196x196.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── favicon.svg │ │ ├── locales │ │ │ ├── ca │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── cs │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── da │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── de │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── en │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── es │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── eu │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── fa │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── fi │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── fr │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── hr │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── hu │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── it │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── ja │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── ko │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── nb │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── nl │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── nn │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── no │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── pl │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── pt-BR │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── pt │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── ru │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── sk │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── sv │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── th │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── tr │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── vi │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ ├── zh-Hant │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ │ └── zh │ │ │ │ ├── blog.json │ │ │ │ ├── common.json │ │ │ │ ├── home.json │ │ │ │ └── pricing.json │ │ ├── logo-grayscale.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ ├── og-image-1200.png │ │ ├── robots.txt │ │ └── static │ │ │ ├── fonts │ │ │ ├── inter-bold.ttf │ │ │ └── inter-regular.ttf │ │ │ ├── images │ │ │ ├── animations.gif │ │ │ ├── device-data.png │ │ │ ├── eric.png │ │ │ ├── goodfirms-logo.svg │ │ │ ├── google.png │ │ │ ├── grouped-times.png │ │ │ ├── hero-shot.png │ │ │ ├── hero.png │ │ │ ├── hubspot-logo.svg │ │ │ ├── introducing-rallly-3-0 │ │ │ │ ├── navigation.png │ │ │ │ ├── poll-status.png │ │ │ │ ├── polls-page.png │ │ │ │ └── updated-scoring.png │ │ │ ├── introducing-rallly-pro │ │ │ │ ├── finalize-poll-demo.gif │ │ │ │ ├── finalize-poll.gif │ │ │ │ ├── pro-splash.svg │ │ │ │ └── rallly-pro-splash.png │ │ │ ├── logo-color.svg │ │ │ ├── luke-vella.jpg │ │ │ ├── mobile-demo.gif │ │ │ ├── partners │ │ │ │ ├── digitalocean-logo.svg │ │ │ │ ├── sentry.svg │ │ │ │ └── vercel-logotype-dark.svg │ │ │ ├── pcmag-logo.svg │ │ │ ├── popsci-logo.svg │ │ │ ├── rallly-pro-launch │ │ │ │ ├── billing-page.png │ │ │ │ ├── email.png │ │ │ │ └── finalize-page.png │ │ │ ├── share-demo.gif │ │ │ ├── shortcut.gif │ │ │ ├── splash.png │ │ │ ├── stars-5.svg │ │ │ ├── the-future-of-rallly │ │ │ │ ├── availability-grid.svg │ │ │ │ ├── funnel.svg │ │ │ │ ├── poll.svg │ │ │ │ └── rsvp.svg │ │ │ ├── timeslots-demo.gif │ │ │ ├── touchable-area.png │ │ │ └── twitter-card.png │ │ │ └── scripts │ │ │ └── mailerlite.js │ ├── src │ │ ├── app │ │ │ ├── [locale] │ │ │ │ ├── (main) │ │ │ │ │ ├── [...notFound] │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── blog │ │ │ │ │ │ ├── [slug] │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ └── post-preview.tsx │ │ │ │ │ ├── cookie-policy │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── footer.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── nav-link.tsx │ │ │ │ │ ├── open-source-banner.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── pricing │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ └── pricing-table.tsx │ │ │ │ │ ├── privacy-policy │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── terms-of-use │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── licensing │ │ │ │ │ └── thank-you │ │ │ │ │ │ └── page.tsx │ │ │ │ └── not-found.tsx │ │ │ ├── api │ │ │ │ └── og-image │ │ │ │ │ └── route.tsx │ │ │ └── sitemap.ts │ │ ├── assets │ │ │ ├── discord.svg │ │ │ ├── github.svg │ │ │ ├── linkedin.svg │ │ │ └── twitter.svg │ │ ├── components │ │ │ ├── blog │ │ │ │ ├── date-formatter.tsx │ │ │ │ ├── post-body.tsx │ │ │ │ └── post-header.tsx │ │ │ ├── error-page.tsx │ │ │ ├── home │ │ │ │ ├── ban-ads.svg │ │ │ │ ├── bonus-item.tsx │ │ │ │ ├── bonus.tsx │ │ │ │ ├── hero.tsx │ │ │ │ └── scribble-arrow.svg │ │ │ └── marketing.tsx │ │ ├── fonts │ │ │ ├── handwritten.ts │ │ │ └── sans.ts │ │ ├── i18n │ │ │ ├── client │ │ │ │ ├── i18n-provider.tsx │ │ │ │ ├── trans.tsx │ │ │ │ └── use-translation.ts │ │ │ ├── server.ts │ │ │ └── settings.ts │ │ ├── lib │ │ │ ├── api.ts │ │ │ └── linkToApp.ts │ │ ├── middleware.ts │ │ ├── posts │ │ │ ├── introducing-quick-create.md │ │ │ ├── introducing-rallly-pro.md │ │ │ ├── july-recap.md │ │ │ ├── mobile-ui-update.md │ │ │ ├── new-version-announcment.md │ │ │ ├── rallly-3-0-self-hosting.md │ │ │ ├── rallly-3-0.md │ │ │ ├── rallly-pro-launch.md │ │ │ └── the-future-of-rallly.md │ │ ├── style.css │ │ └── types.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vercel.json └── web │ ├── .env.test │ ├── .gitignore │ ├── Dockerfile │ ├── declarations │ ├── environment.d.ts │ ├── i18next.d.ts │ └── next-auth.d.ts │ ├── i18n.config.js │ ├── i18next-scanner.config.js │ ├── instrumentation-client.ts │ ├── next-env.d.ts │ ├── next-i18next.config.js │ ├── next.config.js │ ├── package.json │ ├── playwright.config.ts │ ├── postcss.config.js │ ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-167x167.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── bimi-logo.svg │ ├── digitalocean.svg │ ├── favicon-128x128.png │ ├── favicon-16x16.png │ ├── favicon-196x196.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.svg │ ├── images │ │ ├── logo-mark.svg │ │ └── rallly-logo-mark.png │ ├── locales │ │ ├── ca │ │ │ └── app.json │ │ ├── cs │ │ │ └── app.json │ │ ├── da │ │ │ └── app.json │ │ ├── de │ │ │ └── app.json │ │ ├── en │ │ │ └── app.json │ │ ├── es │ │ │ └── app.json │ │ ├── eu │ │ │ └── app.json │ │ ├── fa │ │ │ └── app.json │ │ ├── fi │ │ │ └── app.json │ │ ├── fr │ │ │ └── app.json │ │ ├── hr │ │ │ └── app.json │ │ ├── hu │ │ │ └── app.json │ │ ├── it │ │ │ └── app.json │ │ ├── ja │ │ │ └── app.json │ │ ├── ko │ │ │ └── app.json │ │ ├── nb │ │ │ └── app.json │ │ ├── nl │ │ │ └── app.json │ │ ├── nn │ │ │ └── app.json │ │ ├── no │ │ │ └── app.json │ │ ├── pl │ │ │ └── app.json │ │ ├── pt-BR │ │ │ └── app.json │ │ ├── pt │ │ │ └── app.json │ │ ├── ru │ │ │ └── app.json │ │ ├── sk │ │ │ └── app.json │ │ ├── sv │ │ │ └── app.json │ │ ├── th │ │ │ └── app.json │ │ ├── tr │ │ │ └── app.json │ │ ├── vi │ │ │ └── app.json │ │ ├── zh-Hant │ │ │ └── app.json │ │ └── zh │ │ │ └── app.json │ ├── logo-color.svg │ ├── logo.png │ ├── manifest.json │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ ├── og-image-1200.png │ ├── robots.txt │ └── static │ │ ├── fonts │ │ ├── inter-bold.ttf │ │ └── inter-regular.ttf │ │ ├── google-calendar.svg │ │ ├── google.svg │ │ ├── logo.svg │ │ ├── microsoft-365.svg │ │ ├── microsoft.svg │ │ ├── outlook.svg │ │ └── yahoo.svg │ ├── sentry.edge.config.ts │ ├── sentry.server.config.ts │ ├── src │ ├── actions │ │ └── track-poll-view.ts │ ├── app │ │ ├── [locale] │ │ │ ├── (auth) │ │ │ │ ├── auth │ │ │ │ │ └── login │ │ │ │ │ │ ├── components │ │ │ │ │ │ └── login-page.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── components │ │ │ │ │ └── auth-page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── login │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── auth-errors.tsx │ │ │ │ │ │ ├── login-email-form.tsx │ │ │ │ │ │ ├── login-with-oidc.tsx │ │ │ │ │ │ ├── or-divider.tsx │ │ │ │ │ │ └── sso-provider.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── verify │ │ │ │ │ │ ├── components │ │ │ │ │ │ └── otp-form.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ └── register │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── components │ │ │ │ │ ├── register-name-form.tsx │ │ │ │ │ └── schema.ts │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── verify │ │ │ │ │ ├── components │ │ │ │ │ └── otp-form.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── (space) │ │ │ │ ├── admin-setup │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── make-me-admin-button.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── app-card.tsx │ │ │ │ ├── components │ │ │ │ │ ├── sidebar │ │ │ │ │ │ ├── app-sidebar-provider.tsx │ │ │ │ │ │ ├── app-sidebar.tsx │ │ │ │ │ │ ├── nav-item.tsx │ │ │ │ │ │ └── nav-user.tsx │ │ │ │ │ ├── top-bar.tsx │ │ │ │ │ └── upgrade-button.tsx │ │ │ │ ├── events │ │ │ │ │ ├── events-tabbed-view.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── types.ts │ │ │ │ ├── feedback-alert.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── mobile-navigation.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── polls │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── polls-tabbed-view.tsx │ │ │ │ └── settings │ │ │ │ │ ├── billing │ │ │ │ │ ├── components │ │ │ │ │ │ ├── payment-method.tsx │ │ │ │ │ │ ├── subscription-price.tsx │ │ │ │ │ │ └── subscription-status.tsx │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── components │ │ │ │ │ ├── settings-layout.tsx │ │ │ │ │ └── sign-out-button.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── preferences │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── date-time-preferences.tsx │ │ │ │ │ │ └── language-preference.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── preferences-page.tsx │ │ │ │ │ ├── profile │ │ │ │ │ ├── delete-account-dialog.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── profile-email-address.tsx │ │ │ │ │ ├── profile-picture.tsx │ │ │ │ │ └── profile-settings.tsx │ │ │ │ │ └── settings-menu.tsx │ │ │ ├── [...notFound] │ │ │ │ └── page.tsx │ │ │ ├── admin │ │ │ │ └── [adminUrlId] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── types.ts │ │ │ ├── control-panel │ │ │ │ ├── control-panel-sidebar-provider.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── license │ │ │ │ │ └── page.tsx │ │ │ │ ├── nav-item.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── sidebar.tsx │ │ │ │ └── users │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── delete-user-button.tsx │ │ │ │ │ ├── dialogs │ │ │ │ │ └── delete-user-dialog.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── user-dropdown.tsx │ │ │ │ │ ├── user-row.tsx │ │ │ │ │ ├── user-search-input.tsx │ │ │ │ │ └── users-tabbed-view.tsx │ │ │ ├── invite │ │ │ │ └── [urlId] │ │ │ │ │ ├── invite-page.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── nav.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── providers.tsx │ │ │ ├── layout.tsx │ │ │ ├── new │ │ │ │ ├── back-button.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── not-found.tsx │ │ │ ├── p │ │ │ │ └── [participantUrlId] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── types.ts │ │ │ ├── poll │ │ │ │ └── [urlId] │ │ │ │ │ ├── admin-page.tsx │ │ │ │ │ ├── duplicate-dialog.tsx │ │ │ │ │ ├── duplicate-form.tsx │ │ │ │ │ ├── edit-details │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── edit-options │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── edit-settings │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── guest-poll-alert.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── unsubscribe-alert.tsx │ │ │ ├── quick-create │ │ │ │ └── page.tsx │ │ │ ├── setup │ │ │ │ └── page.tsx │ │ │ ├── timezone-change-detector.tsx │ │ │ └── types.ts │ │ ├── api │ │ │ ├── auth │ │ │ │ ├── [...nextauth] │ │ │ │ │ └── route.ts │ │ │ │ └── invalid-session │ │ │ │ │ └── route.ts │ │ │ ├── house-keeping │ │ │ │ ├── delete-inactive-polls │ │ │ │ │ └── route.ts │ │ │ │ └── remove-deleted-polls │ │ │ │ │ └── route.ts │ │ │ ├── licensing │ │ │ │ └── v1 │ │ │ │ │ └── [...route] │ │ │ │ │ └── route.ts │ │ │ ├── notifications │ │ │ │ └── unsubscribe │ │ │ │ │ └── route.ts │ │ │ ├── og-image-poll │ │ │ │ ├── logo-color.svg │ │ │ │ └── route.tsx │ │ │ ├── send-email │ │ │ │ └── route.ts │ │ │ ├── status │ │ │ │ └── route.ts │ │ │ ├── storage │ │ │ │ └── [...key] │ │ │ │ │ └── route.ts │ │ │ ├── stripe │ │ │ │ ├── buy-license │ │ │ │ │ └── route.ts │ │ │ │ ├── checkout │ │ │ │ │ └── route.ts │ │ │ │ ├── portal │ │ │ │ │ ├── helpers │ │ │ │ │ │ └── create-portal-session.ts │ │ │ │ │ ├── payment-methods │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ └── webhook │ │ │ │ │ ├── handlers │ │ │ │ │ ├── checkout │ │ │ │ │ │ ├── completed.ts │ │ │ │ │ │ ├── expired.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── customer-subscription │ │ │ │ │ │ ├── created.ts │ │ │ │ │ │ ├── deleted.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── updated.ts │ │ │ │ │ ├── customer │ │ │ │ │ │ ├── created.ts │ │ │ │ │ │ ├── deleted.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── payment-method │ │ │ │ │ │ ├── attached.ts │ │ │ │ │ │ ├── detached.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── updated.ts │ │ │ │ │ └── utils.ts │ │ │ │ │ └── route.ts │ │ │ ├── trpc │ │ │ │ └── [trpc] │ │ │ │ │ └── route.ts │ │ │ └── user │ │ │ │ └── verify-email-change │ │ │ │ └── route.ts │ │ ├── components │ │ │ ├── logo-link.tsx │ │ │ ├── logout-button.tsx │ │ │ ├── page-icons.tsx │ │ │ ├── page-layout.tsx │ │ │ ├── redirect.tsx │ │ │ └── search-input.tsx │ │ ├── global-error.tsx │ │ └── posthog-page-view.tsx │ ├── assets │ │ ├── if-need-be.svg │ │ ├── no.svg │ │ ├── pending.svg │ │ └── yes.svg │ ├── auth │ │ ├── adapters │ │ │ └── prisma.ts │ │ ├── edge │ │ │ ├── index.ts │ │ │ └── with-auth.ts │ │ ├── helpers │ │ │ ├── get-optional-providers.ts │ │ │ ├── is-email-blocked.ts │ │ │ ├── merge-user.ts │ │ │ └── temp-email-domains.ts │ │ ├── providers │ │ │ ├── email.ts │ │ │ ├── google.ts │ │ │ ├── guest.ts │ │ │ ├── microsoft.ts │ │ │ ├── oidc.ts │ │ │ └── registration-token.ts │ │ └── queries.ts │ ├── components │ │ ├── add-to-calendar-button.tsx │ │ ├── clock.tsx │ │ ├── container.tsx │ │ ├── cookie-consent.tsx │ │ ├── cookie-consent │ │ │ └── cookies.svg │ │ ├── copy-link-button.tsx │ │ ├── create-poll.tsx │ │ ├── date-card.tsx │ │ ├── date-icon.tsx │ │ ├── description-list.tsx │ │ ├── discussion │ │ │ ├── discussion.tsx │ │ │ └── index.ts │ │ ├── empty-state.tsx │ │ ├── error-page.tsx │ │ ├── event-card.tsx │ │ ├── feedback.tsx │ │ ├── forms │ │ │ ├── index.ts │ │ │ ├── poll-details-form.tsx │ │ │ ├── poll-options-form │ │ │ │ ├── date-navigation-toolbar.tsx │ │ │ │ ├── dayjs-localizer.ts │ │ │ │ ├── index.ts │ │ │ │ ├── month-calendar │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── month-calendar.tsx │ │ │ │ │ └── time-picker.tsx │ │ │ │ ├── poll-options-form.tsx │ │ │ │ ├── rbc-overrides.css │ │ │ │ ├── types.ts │ │ │ │ ├── utils.ts │ │ │ │ └── week-calendar.tsx │ │ │ ├── poll-settings.tsx │ │ │ └── types.ts │ │ ├── headless-date-picker.tsx │ │ ├── input-otp.tsx │ │ ├── invite-dialog.tsx │ │ ├── layouts │ │ │ ├── poll-layout.tsx │ │ │ ├── timeformat.tsx │ │ │ └── timezone-control.tsx │ │ ├── login-link.tsx │ │ ├── logo.tsx │ │ ├── maintenance.tsx │ │ ├── modal │ │ │ ├── index.ts │ │ │ ├── modal-provider.tsx │ │ │ ├── modal.tsx │ │ │ └── use-modal.tsx │ │ ├── new-participant-modal.tsx │ │ ├── optimized-avatar-image.tsx │ │ ├── page-dialog.tsx │ │ ├── pagination.tsx │ │ ├── participant-avatar-bar.tsx │ │ ├── participant-dropdown.tsx │ │ ├── participant.tsx │ │ ├── participants-provider.tsx │ │ ├── pay-wall-dialog.tsx │ │ ├── poll-context.tsx │ │ ├── poll-status.tsx │ │ ├── poll │ │ │ ├── desktop-poll.tsx │ │ │ ├── desktop-poll │ │ │ │ ├── participant-row-form.tsx │ │ │ │ ├── participant-row.tsx │ │ │ │ └── poll-header.tsx │ │ │ ├── guest-alert.tsx │ │ │ ├── language-selector.tsx │ │ │ ├── manage-poll.tsx │ │ │ ├── manage-poll │ │ │ │ ├── delete-poll-dialog.tsx │ │ │ │ ├── finalize-poll-dialog.tsx │ │ │ │ └── use-csv-exporter.ts │ │ │ ├── mobile-poll.tsx │ │ │ ├── mobile-poll │ │ │ │ ├── date-option.tsx │ │ │ │ ├── grouped-options.tsx │ │ │ │ ├── poll-option.tsx │ │ │ │ ├── poll-options.tsx │ │ │ │ └── time-slot-option.tsx │ │ │ ├── mutations.ts │ │ │ ├── notifications-toggle.tsx │ │ │ ├── poll-context-provider.tsx │ │ │ ├── poll-footer.tsx │ │ │ ├── poll-header.tsx │ │ │ ├── poll-view-tracker.tsx │ │ │ ├── poll-viz.tsx │ │ │ ├── responsive-results.tsx │ │ │ ├── scheduled-event.tsx │ │ │ ├── score-summary.tsx │ │ │ ├── truncated-linkify.tsx │ │ │ ├── types.ts │ │ │ ├── vote-icon.tsx │ │ │ ├── vote-selector.tsx │ │ │ ├── voting-form.tsx │ │ │ └── you-avatar.tsx │ │ ├── pro-badge.tsx │ │ ├── random-gradient-bar.tsx │ │ ├── register-link.tsx │ │ ├── relative-date.tsx │ │ ├── router-loading-indicator.tsx │ │ ├── skeleton.tsx │ │ ├── spinner.tsx │ │ ├── stacked-list.tsx │ │ ├── steps │ │ │ ├── index.ts │ │ │ ├── step.tsx │ │ │ └── steps.tsx │ │ ├── table │ │ │ └── table-column-header.tsx │ │ ├── time-format-picker.tsx │ │ ├── time-zone-picker │ │ │ └── time-zone-select.tsx │ │ ├── trans.tsx │ │ ├── upgrade-button.tsx │ │ ├── use-required-context.ts │ │ ├── user-avatar │ │ │ └── gradients.ts │ │ ├── user-dropdown.tsx │ │ ├── user-provider.tsx │ │ ├── user.tsx │ │ ├── visibility-trigger.tsx │ │ ├── visibility.tsx │ │ ├── vote-icon │ │ │ ├── if-need-be-icon.tsx │ │ │ ├── no-icon.tsx │ │ │ ├── pending-icon.tsx │ │ │ └── yes-icon.tsx │ │ └── vote-summary-progress-bar.tsx │ ├── contexts │ │ ├── environment.tsx │ │ ├── permissions.tsx │ │ ├── poll.tsx │ │ ├── preferences.tsx │ │ └── role.tsx │ ├── env.ts │ ├── features │ │ ├── feature-flags │ │ │ └── client.tsx │ │ ├── feedback │ │ │ ├── actions.ts │ │ │ ├── components │ │ │ │ └── feedback-toggle.tsx │ │ │ └── schema.ts │ │ ├── licensing │ │ │ ├── actions │ │ │ │ └── validate-license.ts │ │ │ ├── client.ts │ │ │ ├── components │ │ │ │ ├── license-key-form.tsx │ │ │ │ ├── license-limit-warning.tsx │ │ │ │ └── remove-license-button.tsx │ │ │ ├── helpers │ │ │ │ ├── calculate-checksum.ts │ │ │ │ ├── check-license-key.ts │ │ │ │ └── generate-license-key.ts │ │ │ ├── index.ts │ │ │ ├── lib │ │ │ │ └── licensing-client.ts │ │ │ ├── mutations.ts │ │ │ ├── queries.ts │ │ │ └── schema.ts │ │ ├── moderation │ │ │ ├── index.ts │ │ │ └── libs │ │ │ │ ├── ai-moderation.ts │ │ │ │ └── pattern-moderation.ts │ │ ├── navigation │ │ │ └── command-menu │ │ │ │ ├── command-global-shortcut.tsx │ │ │ │ ├── command-menu.tsx │ │ │ │ └── index.ts │ │ ├── poll │ │ │ ├── api │ │ │ │ ├── get-polls.ts │ │ │ │ └── get-recently-updated-polls.ts │ │ │ ├── components │ │ │ │ ├── poll-list.tsx │ │ │ │ └── poll-status-icon.tsx │ │ │ └── schema.ts │ │ ├── quick-create │ │ │ ├── components │ │ │ │ └── relative-date.tsx │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── lib │ │ │ │ └── get-guest-polls.ts │ │ │ ├── quick-create-button.tsx │ │ │ └── quick-create-widget.tsx │ │ ├── rate-limit │ │ │ ├── constants.ts │ │ │ └── index.ts │ │ ├── scheduled-event │ │ │ ├── api │ │ │ │ └── get-scheduled-events.ts │ │ │ ├── components │ │ │ │ ├── scheduled-event-list.tsx │ │ │ │ └── scheduled-event-status-badge.tsx │ │ │ └── schema.ts │ │ ├── setup │ │ │ ├── actions.ts │ │ │ ├── components │ │ │ │ └── setup-form.tsx │ │ │ ├── queries.ts │ │ │ ├── schema.ts │ │ │ └── types.ts │ │ ├── storage │ │ │ ├── index.ts │ │ │ └── s3.ts │ │ ├── subscription │ │ │ └── schema.ts │ │ ├── timezone │ │ │ ├── client │ │ │ │ ├── calendar-date.tsx │ │ │ │ ├── context.tsx │ │ │ │ └── formatted-date-time.tsx │ │ │ ├── formatted-date-time-server.tsx │ │ │ ├── hooks │ │ │ │ └── use-formatted-date-time.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ └── user │ │ │ ├── queries.ts │ │ │ └── schema.ts │ ├── global-error.tsx │ ├── i18n │ │ ├── client.tsx │ │ ├── i18n.ts │ │ ├── server.ts │ │ ├── server │ │ │ └── get-locale.ts │ │ ├── settings.ts │ │ └── types.ts │ ├── instrumentation.ts │ ├── middleware.ts │ ├── next-auth.config.ts │ ├── next-auth.ts │ ├── style.css │ ├── trpc │ │ ├── client.ts │ │ ├── client │ │ │ ├── provider.tsx │ │ │ └── types.ts │ │ ├── context.ts │ │ ├── routers │ │ │ ├── auth.ts │ │ │ ├── dashboard.ts │ │ │ ├── index.ts │ │ │ ├── polls.ts │ │ │ ├── polls │ │ │ │ ├── comments.ts │ │ │ │ └── participants.ts │ │ │ ├── scheduled-events.ts │ │ │ └── user.ts │ │ ├── server │ │ │ └── create-ssr-helper.ts │ │ ├── trpc.ts │ │ └── types.ts │ ├── types.ts │ └── utils │ │ ├── api-auth.ts │ │ ├── color-hash.ts │ │ ├── constants.ts │ │ ├── cookies.ts │ │ ├── date-time-utils.ts │ │ ├── date-time-utilts.test.ts │ │ ├── date.ts │ │ ├── dayjs.tsx │ │ ├── emails.ts │ │ ├── form-validation.ts │ │ ├── get-value-by-path.test.ts │ │ ├── get-value-by-path.ts │ │ ├── grouped-time-zone.ts │ │ ├── is-valid-name.ts │ │ ├── local-storage.ts │ │ ├── next.ts │ │ ├── permissions.ts │ │ ├── posthog.ts │ │ ├── selectors.ts │ │ ├── session │ │ ├── index.ts │ │ └── session-config.ts │ │ ├── subscription.ts │ │ └── supported-time-zones.ts │ ├── tailwind.config.js │ ├── tests │ ├── admin-setup.spec.ts │ ├── authentication.spec.ts │ ├── create-delete-poll.spec.ts │ ├── edit-options-page.ts │ ├── edit-options.spec.ts │ ├── guest-to-user-migration.spec.ts │ ├── helpers │ │ └── next-auth-v4.ts │ ├── house-keeping.spec.ts │ ├── i18n.spec.ts │ ├── invite-page.ts │ ├── login-page.ts │ ├── mailpit │ │ ├── mailpit.ts │ │ └── types.ts │ ├── mobile-test.spec.ts │ ├── new-poll-page.ts │ ├── poll-page.ts │ ├── register-page.ts │ ├── test-utils.ts │ ├── timezone-change.spec.ts │ ├── utils.ts │ └── vote-and-comment.spec.ts │ ├── tsconfig.json │ ├── vercel.json │ └── vitest.config.mts ├── assets └── images │ ├── appwrite.svg │ ├── cloudron-logo.svg │ ├── digitalocean-logo.svg │ ├── featurebase.svg │ ├── logo-color.svg │ ├── sentry.svg │ ├── splash.png │ ├── ura-logo-blue.svg │ ├── ura-logo-white.svg │ ├── vercel-logotype-dark.svg │ └── vercel-logotype-light.svg ├── biome.json ├── crowdin.yml ├── docker-compose.dev.yml ├── docker-compose.yml ├── package.json ├── packages ├── billing │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── get-pricing.ts │ │ │ └── stripe.ts │ │ ├── pricing.ts │ │ └── scripts │ │ │ ├── checkout-expiry.ts │ │ │ ├── subscription-data-sync.ts │ │ │ └── sync-payment-methods.ts │ └── tsconfig.json ├── database │ ├── .gitignore │ ├── index.ts │ ├── package.json │ ├── prisma │ │ ├── migrations │ │ │ ├── 20220315143815_init │ │ │ │ └── migration.sql │ │ │ ├── 20220322193753_add_comments │ │ │ │ └── migration.sql │ │ │ ├── 20220329102907_add_links │ │ │ │ └── migration.sql │ │ │ ├── 20220330085423_add_notifications_column │ │ │ │ └── migration.sql │ │ │ ├── 20220408120721_legacy_column │ │ │ │ └── migration.sql │ │ │ ├── 20220412112814_cascade_delete │ │ │ │ └── migration.sql │ │ │ ├── 20220412115407_cascade_delete_participants │ │ │ │ └── migration.sql │ │ │ ├── 20220412115744_cascade_delete_votes │ │ │ │ └── migration.sql │ │ │ ├── 20220414101318_remove_duplicate_emails │ │ │ │ └── migration.sql │ │ │ ├── 20220506105524_sessions_update │ │ │ │ └── migration.sql │ │ │ ├── 20220511113020_add_if_need_be │ │ │ │ └── migration.sql │ │ │ ├── 20220511180705_naming_convention_update │ │ │ │ └── migration.sql │ │ │ ├── 20220512093441_add_no_votes │ │ │ │ └── migration.sql │ │ │ ├── 20220519075453_add_delete_column │ │ │ │ └── migration.sql │ │ │ ├── 20220520115326_add_touch_column │ │ │ │ └── migration.sql │ │ │ ├── 20220522153812_change_referential_integrity │ │ │ │ └── migration.sql │ │ │ ├── 20220522165453_add_deleted_at_column │ │ │ │ └── migration.sql │ │ │ ├── 20220623175037_remove_links_model │ │ │ │ └── migration.sql │ │ │ ├── 20220624111614_rename_poll_id │ │ │ │ └── migration.sql │ │ │ ├── 20220627191901_remove_user_relation │ │ │ │ └── migration.sql │ │ │ ├── 20221026220835_add_indexes │ │ │ │ └── migration.sql │ │ │ ├── 20230117103853_update_indexes │ │ │ │ └── migration.sql │ │ │ ├── 20230118192253_bring_back_comment_index │ │ │ │ └── migration.sql │ │ │ ├── 20230118200546_add_unqiue_participant │ │ │ │ └── migration.sql │ │ │ ├── 20230303142641_add_participant_email │ │ │ │ └── migration.sql │ │ │ ├── 20230318113511_remove_unecessary_unique_indexes │ │ │ │ └── migration.sql │ │ │ ├── 20230322143831_watchers │ │ │ │ └── migration.sql │ │ │ ├── 20230327105647_remove_author_name │ │ │ │ └── migration.sql │ │ │ ├── 20230329173551_options_refactor │ │ │ │ └── migration.sql │ │ │ ├── 20230614141138_v3 │ │ │ │ └── migration.sql │ │ │ ├── 20230615111329_events │ │ │ │ └── migration.sql │ │ │ ├── 20230615163229_remove_selected_option_id │ │ │ │ └── migration.sql │ │ │ ├── 20230704153346_user_payments │ │ │ │ └── migration.sql │ │ │ ├── 20230721163042_hide_participants │ │ │ │ └── migration.sql │ │ │ ├── 20230725112615_add_poll_config │ │ │ │ └── migration.sql │ │ │ ├── 20230823084154_stripe_subscriptions │ │ │ │ └── migration.sql │ │ │ ├── 20230915170216_add_required_email │ │ │ │ └── migration.sql │ │ │ ├── 20231016093654_next_auth │ │ │ │ └── migration.sql │ │ │ ├── 20231027074632_nextauth_ci_identifiers │ │ │ │ └── migration.sql │ │ │ ├── 20231117153753_add_nextauth_provider_accounts │ │ │ │ └── migration.sql │ │ │ ├── 20231118134458_add_account_user_index │ │ │ │ └── migration.sql │ │ │ ├── 20231122061137_map_account_table_names │ │ │ │ └── migration.sql │ │ │ ├── 20231205043530_poll_status │ │ │ │ └── migration.sql │ │ │ ├── 20231205080854_fix_finalized_poll_status │ │ │ │ └── migration.sql │ │ │ ├── 20240127064213_add_image_field │ │ │ │ └── migration.sql │ │ │ ├── 20240221084400_unset_invalid_timezones │ │ │ │ └── migration.sql │ │ │ ├── 20240224011353_remove_legacy_user_preferences │ │ │ │ └── migration.sql │ │ │ ├── 20240224033808_option_start_time │ │ │ │ └── migration.sql │ │ │ ├── 20240309043111_remove_poll_vote_index │ │ │ │ └── migration.sql │ │ │ ├── 20240309051319_add_option_vote_index │ │ │ │ └── migration.sql │ │ │ ├── 20240315104329_index_votes_by_poll │ │ │ │ └── migration.sql │ │ │ ├── 20240317095541_remove_legacy_start_column │ │ │ │ └── migration.sql │ │ │ ├── 20240704085250_soft_delete_participants │ │ │ │ └── migration.sql │ │ │ ├── 20240901171230_participant_locale │ │ │ │ └── migration.sql │ │ │ ├── 20241224103150_guest_id_column │ │ │ │ └── migration.sql │ │ │ ├── 20241228093234_update_relational_model │ │ │ │ └── migration.sql │ │ │ ├── 20250217082042_add_subscription_fields │ │ │ │ └── migration.sql │ │ │ ├── 20250217103625_make_subscription_fields_required │ │ │ │ └── migration.sql │ │ │ ├── 20250221104854_add_cancel_at_period_end │ │ │ │ └── migration.sql │ │ │ ├── 20250221111248_add_subscription_interval_enum │ │ │ │ └── migration.sql │ │ │ ├── 20250222172325_add_payment_method_table │ │ │ │ └── migration.sql │ │ │ ├── 20250223160003_set_subscription_status_enum │ │ │ │ └── migration.sql │ │ │ ├── 20250226173250_remove_paddle_subscriptions │ │ │ │ └── migration.sql │ │ │ ├── 20250227110115_make_subscription_user_required │ │ │ │ └── migration.sql │ │ │ ├── 20250302163530_add_user_ban_fields │ │ │ │ └── migration.sql │ │ │ ├── 20250327193922_track_poll_views │ │ │ │ └── migration.sql │ │ │ ├── 20250402100733_remove_closed_column │ │ │ │ └── migration.sql │ │ │ ├── 20250415153024_scheduled_events │ │ │ │ └── migration.sql │ │ │ ├── 20250415155219_migrate_events │ │ │ │ └── migration.sql │ │ │ ├── 20250421170921_migrate_invites │ │ │ │ └── migration.sql │ │ │ ├── 20250522093338_add_user_role │ │ │ │ └── migration.sql │ │ │ ├── 20250522165415_licensing_schema │ │ │ │ └── migration.sql │ │ │ ├── 20250523161200_remove_invitee_constraints │ │ │ │ └── migration.sql │ │ │ ├── 20250526175615_remove_stripe_customer_id │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ ├── models │ │ │ ├── billing.prisma │ │ │ ├── event.prisma │ │ │ ├── licensing.prisma │ │ │ ├── poll.prisma │ │ │ └── user.prisma │ │ ├── schema.prisma │ │ ├── seed.ts │ │ └── seed │ │ │ ├── polls.ts │ │ │ ├── scheduled-events.ts │ │ │ ├── users.ts │ │ │ └── utils.ts │ └── tsconfig.json ├── emails │ ├── .gitignore │ ├── i18next-scanner.config.js │ ├── i18next.d.ts │ ├── locales │ │ ├── ca │ │ │ └── emails.json │ │ ├── cs │ │ │ └── emails.json │ │ ├── da │ │ │ └── emails.json │ │ ├── de │ │ │ └── emails.json │ │ ├── en │ │ │ └── emails.json │ │ ├── es │ │ │ └── emails.json │ │ ├── eu │ │ │ └── emails.json │ │ ├── fi │ │ │ └── emails.json │ │ ├── fr │ │ │ └── emails.json │ │ ├── hr │ │ │ └── emails.json │ │ ├── hu │ │ │ └── emails.json │ │ ├── it │ │ │ └── emails.json │ │ ├── ja │ │ │ └── emails.json │ │ ├── ko │ │ │ └── emails.json │ │ ├── nl │ │ │ └── emails.json │ │ ├── no │ │ │ └── emails.json │ │ ├── pl │ │ │ └── emails.json │ │ ├── pt-BR │ │ │ └── emails.json │ │ ├── pt │ │ │ └── emails.json │ │ ├── ru │ │ │ └── emails.json │ │ ├── sk │ │ │ └── emails.json │ │ ├── sv │ │ │ └── emails.json │ │ ├── th │ │ │ └── emails.json │ │ ├── tr │ │ │ └── emails.json │ │ ├── vi │ │ │ └── emails.json │ │ ├── zh-Hant │ │ │ └── emails.json │ │ └── zh │ │ │ └── emails.json │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── email-context.tsx │ │ │ ├── email-layout.tsx │ │ │ ├── notification-email.tsx │ │ │ └── styled-components.tsx │ │ ├── i18n.ts │ │ ├── index.ts │ │ ├── previews │ │ │ ├── abandoned-checkout.tsx │ │ │ ├── change-email-request.tsx │ │ │ ├── finalized-host.tsx │ │ │ ├── finalized-participant.tsx │ │ │ ├── license-key.tsx │ │ │ ├── login.tsx │ │ │ ├── new-comment.tsx │ │ │ ├── new-participant-confirmation.tsx │ │ │ ├── new-participant.tsx │ │ │ ├── new-poll.tsx │ │ │ ├── register.tsx │ │ │ └── static │ │ │ │ └── logo.png │ │ ├── send-email.tsx │ │ ├── templates.ts │ │ ├── templates │ │ │ ├── abandoned-checkout.tsx │ │ │ ├── change-email-request.tsx │ │ │ ├── finalized-host.tsx │ │ │ ├── finalized-participant.tsx │ │ │ ├── license-key.tsx │ │ │ ├── login.tsx │ │ │ ├── new-comment.tsx │ │ │ ├── new-participant-confirmation.tsx │ │ │ ├── new-participant.tsx │ │ │ ├── new-poll.tsx │ │ │ └── register.tsx │ │ └── types.ts │ └── tsconfig.json ├── languages │ ├── package.json │ ├── src │ │ ├── get-preferred-locale.ts │ │ ├── index.ts │ │ └── languages.ts │ └── tsconfig.json ├── posthog │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── constants.ts │ │ ├── server │ │ │ └── index.ts │ │ └── utils.ts │ └── tsconfig.json ├── tailwind-config │ ├── package.json │ ├── tailwind.config.d.ts │ └── tailwind.config.js ├── tsconfig │ ├── next.json │ ├── node.json │ ├── package.json │ └── react.json ├── ui │ ├── components.json │ ├── custom.d.ts │ ├── package.json │ ├── src │ │ ├── accordion.tsx │ │ ├── action-bar.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── billing-plan.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox-check.svg │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dot-pattern.tsx │ │ ├── dropdown-menu.tsx │ │ ├── flex.tsx │ │ ├── form.tsx │ │ ├── helpers │ │ │ └── component-props.ts │ │ ├── hooks │ │ │ ├── use-mobile.ts │ │ │ ├── use-platform.ts │ │ │ └── use-toast.ts │ │ ├── icon.tsx │ │ ├── index.ts │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── lib │ │ │ └── utils.ts │ │ ├── page-tabs.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── radio-pills.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── styles │ │ │ └── globals.css │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── text-field.tsx │ │ ├── textarea.tsx │ │ ├── tile.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── tooltip.tsx │ ├── tailwind.config.js │ └── tsconfig.json └── utils │ ├── index.ts │ ├── package.json │ ├── src │ ├── absolute-url.test.ts │ ├── absolute-url.ts │ ├── nanoid.ts │ ├── prevent-widows.ts │ ├── safe-session-storage.ts │ └── sleep.ts │ ├── tsconfig.json │ └── vitest.config.mts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── create-release.sh ├── docker-start.sh └── inject-version.js ├── turbo.json └── vitest.workspace.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | .github 4 | .vscode 5 | tsconfig.tsbuildinfo 6 | .env 7 | .sentryclirc 8 | sentry.properties 9 | Dockerfile 10 | docker-compose.yml 11 | /docs 12 | README.md 13 | node_modules 14 | 15 | # Build outputs 16 | apps/*/.next 17 | apps/*/dist 18 | packages/*/dist 19 | 20 | # Turbo prune output 21 | out/ 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [lukevella] 4 | custom: paypal.me/ralllyco 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F914 Feature request" 3 | about: Please use discussions instead of issues for feature requests 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **DO NOT OPEN AN ISSUE FOR FEATURE REQUESTS** 11 | 12 | Please use Discussions ☝️ instead and start a new idea discussion if it doesn't exist already. 13 | 14 | Thank you :) 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | **Replace me with a summary of the change and which issue is fixed. Please also include relevant motivation and context.** 4 | 5 | ## Checklist 6 | 7 | Please check off all the following items with an "x" in the boxes before requesting a review. 8 | 9 | - [ ] I have performed a self-review of my code 10 | - [ ] My code follows the code style of this project 11 | - [ ] I have commented my code, particularly in hard-to-understand areas 12 | -------------------------------------------------------------------------------- /.github/actions/pnpm-install/action.yml: -------------------------------------------------------------------------------- 1 | name: "Pnpm Install" 2 | description: "Runs pnpm install with --frozen-lockfile" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Run pnpm install 7 | run: pnpm install --frozen-lockfile 8 | shell: bash 9 | -------------------------------------------------------------------------------- /.github/actions/setup-node/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Node.js" 2 | description: "Sets up a consistent Node.js environment" 3 | inputs: 4 | node-version: 5 | description: "Node.js version" 6 | required: true 7 | default: "20" 8 | cache: 9 | description: "Package manager for caching" 10 | required: false 11 | default: "pnpm" 12 | runs: 13 | using: "composite" 14 | steps: 15 | - name: Setup pnpm 16 | uses: pnpm/action-setup@v4 17 | 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ inputs.node-version }} 22 | cache: ${{ inputs.cache }} 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*" 5 | 6 | name: Create Release 7 | 8 | jobs: 9 | build: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Trigger deploy to production 17 | run: | 18 | curl -X POST -d {} ${{ secrets.DEPLOY_HOOK }} 19 | 20 | - name: Create Release 21 | uses: ncipollo/release-action@v1 22 | with: 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | draft: true 25 | generateReleaseNotes: true 26 | makeLatest: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | .next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | 25 | # local env files 26 | .env 27 | .env*.local 28 | 29 | # ts 30 | tsconfig.tsbuildinfo 31 | 32 | # Turbo 33 | .turbo 34 | 35 | # Jetbrains IDE 36 | .idea 37 | 38 | # Playwright 39 | playwright-report 40 | test-results -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*import-in-the-middle* 2 | public-hoist-pattern[]=*require-in-the-middle* -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /apps/docs/administrators/deleting-a-meeting-poll.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Delete a Poll" 3 | --- 4 | 5 | 6 | Deleting a poll will delete all data related to that poll such as options, 7 | participants and votes. This action cannot be undone. 8 | 9 | 10 | To delete a meeting poll, from the admin page: 11 | 12 | 1. Click **Manage** 13 | 1. Select **Delete poll** from the dropdown menu 14 | 1. Click **Delete poll** 15 | -------------------------------------------------------------------------------- /apps/docs/contact/support.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Support 3 | description: Get in touch with us if you need help with anything 4 | --- 5 | 6 | If you have any questions, feedback or suggestions, please get in touch. We'd love to hear from you! 7 | 8 | 9 | 14 | Send an email 15 | 16 | 21 | Join Discord 22 | 23 | 24 | 25 | 26 | Check out the [FAQ](/faq) page to see if your question has already been 27 | answered. 28 | 29 | -------------------------------------------------------------------------------- /apps/docs/contribute/documentation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Documentation 3 | icon: book 4 | description: Help us maintain and improve our documentation 5 | --- 6 | 7 | If you have a [Github](https://github.com) account, you can make changes or report issues using the links that appear at the top of each page. 8 | 9 | 10 | Edit Documentation Links 14 | 15 | 16 | ## Submitting a Pull Request 17 | 18 | Submitting a pull request is a great way to fix minor issues like typos or broken links. 19 | Click on the "pencil" icon and fork our repository to submit a pull request. 20 | 21 | ## Creating an Issue 22 | 23 | For larger tasks, you can create a new issue by clicking on the "exclamation" icon. 24 | -------------------------------------------------------------------------------- /apps/docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/favicon.png -------------------------------------------------------------------------------- /apps/docs/images/contribute/crowdin-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/contribute/crowdin-project.png -------------------------------------------------------------------------------- /apps/docs/images/contribute/edit-documentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/contribute/edit-documentation.png -------------------------------------------------------------------------------- /apps/docs/images/contribute/icu-message-format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/contribute/icu-message-format.png -------------------------------------------------------------------------------- /apps/docs/images/guide/voting-in-progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/guide/voting-in-progress.png -------------------------------------------------------------------------------- /apps/docs/images/guide/voting-submitted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/guide/voting-submitted.png -------------------------------------------------------------------------------- /apps/docs/images/guide/voting-submitting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/guide/voting-submitting.png -------------------------------------------------------------------------------- /apps/docs/images/home.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/home.jpeg -------------------------------------------------------------------------------- /apps/docs/images/self-hosting/control-panel.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/self-hosting/control-panel.jpeg -------------------------------------------------------------------------------- /apps/docs/images/unused/polls-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/unused/polls-page.png -------------------------------------------------------------------------------- /apps/docs/images/unused/response/editing-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/unused/response/editing-response-1.png -------------------------------------------------------------------------------- /apps/docs/images/unused/response/editing-response-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/unused/response/editing-response-2.png -------------------------------------------------------------------------------- /apps/docs/images/unused/response/editing-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/unused/response/editing-response.png -------------------------------------------------------------------------------- /apps/docs/images/unused/response/submitting-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/unused/response/submitting-response-1.png -------------------------------------------------------------------------------- /apps/docs/images/unused/response/submitting-response-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/unused/response/submitting-response-2.png -------------------------------------------------------------------------------- /apps/docs/images/unused/support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/unused/support.png -------------------------------------------------------------------------------- /apps/docs/images/workflow/finalize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/workflow/finalize.png -------------------------------------------------------------------------------- /apps/docs/images/workflow/finalized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/workflow/finalized.png -------------------------------------------------------------------------------- /apps/docs/images/workflow/invite-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/workflow/invite-link.png -------------------------------------------------------------------------------- /apps/docs/images/workflow/month-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/workflow/month-view.png -------------------------------------------------------------------------------- /apps/docs/images/workflow/new-poll-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/workflow/new-poll-page.png -------------------------------------------------------------------------------- /apps/docs/images/workflow/review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/workflow/review.png -------------------------------------------------------------------------------- /apps/docs/images/workflow/voting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/workflow/voting.png -------------------------------------------------------------------------------- /apps/docs/images/workflow/week-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/docs/images/workflow/week-view.png -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rallly/docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "mint dev", 7 | "build": "mint build" 8 | }, 9 | "devDependencies": { 10 | "@rallly/tsconfig": "workspace:*" 11 | }, 12 | "dependencies": { 13 | "mint": "^4.1.33", 14 | "react": "^19.1.0", 15 | "react-dom": "^19.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/landing/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # playwright 4 | /playwright-report 5 | /test-results -------------------------------------------------------------------------------- /apps/landing/declarations/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import "i18next"; 2 | 3 | import type blog from "../public/locales/en/blog.json"; 4 | import type common from "../public/locales/en/common.json"; 5 | import type home from "../public/locales/en/home.json"; 6 | import type pricing from "../public/locales/en/pricing.json"; 7 | 8 | declare module "i18next" { 9 | interface CustomTypeOptions { 10 | defaultNS: "common"; 11 | resources: { 12 | common: typeof common; 13 | home: typeof home; 14 | pricing: typeof pricing; 15 | blog: typeof blog; 16 | }; 17 | returnNull: false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/landing/i18next-scanner.config.js: -------------------------------------------------------------------------------- 1 | const typescriptTransform = require("i18next-scanner-typescript"); 2 | 3 | module.exports = { 4 | input: ["src/**/*.{ts,tsx}"], 5 | options: { 6 | nsSeparator: ":", 7 | defaultNs: "common", 8 | lngs: ["en"], 9 | ns: ["common", "home", "pricing", "blog"], 10 | plural: false, 11 | removeUnusedKeys: true, 12 | func: { 13 | list: ["t"], 14 | extensions: [".js", ".jsx"], 15 | }, 16 | trans: { 17 | extensions: [".js", ".jsx"], 18 | }, 19 | resource: { 20 | loadPath: "public/locales/{{lng}}/{{ns}}.json", 21 | savePath: "public/locales/{{lng}}/{{ns}}.json", 22 | }, 23 | }, 24 | format: "json", 25 | fallbackLng: "en", 26 | transform: typescriptTransform(), 27 | }; 28 | -------------------------------------------------------------------------------- /apps/landing/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/landing/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["tailwindcss", "autoprefixer"], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/landing/public/.well-known/microsoft-identity-association.json: -------------------------------------------------------------------------------- 1 | { 2 | "associatedApplications": [ 3 | { 4 | "applicationId": "8a0e84e2-4719-4a04-ab6d-30a95a6371d7" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/landing/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/landing/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-167x167.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /apps/landing/public/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /apps/landing/public/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/favicon-128x128.png -------------------------------------------------------------------------------- /apps/landing/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/favicon-16x16.png -------------------------------------------------------------------------------- /apps/landing/public/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/favicon-196x196.png -------------------------------------------------------------------------------- /apps/landing/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/favicon-32x32.png -------------------------------------------------------------------------------- /apps/landing/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/favicon-96x96.png -------------------------------------------------------------------------------- /apps/landing/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/favicon.ico -------------------------------------------------------------------------------- /apps/landing/public/locales/ca/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Publicacions recents", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Notícies, actualitzacions i anuncis sobre Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/cs/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Poslední příspěvky", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Novinky, aktualizace a oznámení o Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/da/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Seneste indlæg", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Nyheder, opdateringer og annoncering om Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/de/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Neueste Beiträge", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "News, Updates und Ankündigung über Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/en/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Recent Posts", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "News, updates and announcement about Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/es/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Posts recientes", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Noticias, actualizaciones y anuncios acerca de Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/eu/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Azken mezuak", 3 | "blogTitle": "Rallly - Bloga", 4 | "blogDescription": "Ralllyri buruzko albisteak, eguneratzeak eta iragarkia." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/fa/blog.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/fa/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "ورود", 3 | "links": "پیوندها", 4 | "blog": "وبلاگ", 5 | "discussions": "بحث‌وگفتگو", 6 | "footerCredit": "ساخته‌شده توسط @imlukevella", 7 | "footerSponsor": "این پروژه توسط کاربران تأمین مالی می‌شود. لطفا با حمایت مالی از آن پشتیبانی کنید.", 8 | "language": "زبان", 9 | "poweredBy": "قدرت گرفته از", 10 | "privacyPolicy": "سیاست رازداری", 11 | "support": "پشتیبانی", 12 | "volunteerTranslator": "کمک به ترجمه‌ی این سایت", 13 | "notFoundTitle": "خطای ۴۰۴ - پیدا نشد", 14 | "notFoundDescription": "متأسفانه صفحه مورد نظر شما یافت نشد.", 15 | "goToHome": "رفتن به خانه" 16 | } 17 | -------------------------------------------------------------------------------- /apps/landing/public/locales/fa/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "startUsOnGithub": "به ما در گیت‌هاب ستاره دهید", 3 | "noLoginRequired": "بدون نیاز به ورود", 4 | "metaDescription": "با ایجاد نظرسنجی بهترین روز و وقت را پیدا کنید! جایگزینی رایگان برای Doodle", 5 | "getStarted": "همین حالا شروع کنید" 6 | } 7 | -------------------------------------------------------------------------------- /apps/landing/public/locales/fa/pricing.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/fi/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Viimeisimmät julkaisut", 3 | "blogTitle": "Rallly - Blogi", 4 | "blogDescription": "Uutisia, päivityksiä ja tiedotteita Ralllysta." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/fr/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Publications récentes", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Actualités, mises à jour et annonces sur Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/hr/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Nedavne objave", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Novosti i obavijesti o Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/hu/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Legutóbbi bejegyzések", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Hírek, frissítések és bejelentések a Rallly-ról." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/it/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Post Recenti", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Notizie, aggiornamenti e annunci su Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/ja/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "最近の投稿", 3 | "blogTitle": "Rallly - ブログ", 4 | "blogDescription": "Ralllyに関するニュース・アップデート・お知らせ。" 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/ko/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "최근 게시물", 3 | "blogTitle": "Rallly - 블로그", 4 | "blogDescription": "Rallly에 관한 뉴스, 업데이트 및 공지사항." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/nb/blog.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/nb/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/nb/home.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/nb/pricing.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/nl/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Recente berichten", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Nieuws, updates en aankondigingen over Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/nn/blog.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/nn/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/nn/home.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/nn/pricing.json: -------------------------------------------------------------------------------- 1 | { 2 | "monthlyBillingDescription": "per månad" 3 | } 4 | -------------------------------------------------------------------------------- /apps/landing/public/locales/no/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Nylige innlegg", 3 | "blogTitle": "Rallly - Blogg", 4 | "blogDescription": "Nyheter, oppdateringer og kunngjøringer om Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/pl/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Ostatnie posty", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Wiadomości, aktualizacje i ogłoszenia dotyczące Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/pt-BR/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Publicações Recentes", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Notícias, atualizações e avisos sobre Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/pt/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Publicações recentes", 3 | "blogTitle": "Rallly - Blogue", 4 | "blogDescription": "Notícias, atualizações e anúncios sobre Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/ru/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Последние сообщения", 3 | "blogTitle": "Rallly - Блог", 4 | "blogDescription": "Новости, обновления и анонсы о Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/sk/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Nedávne príspevky", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Novinky, aktualizácie a oznámenia o Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/sv/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Senaste inläggen", 3 | "blogTitle": "Rallly - Blogg", 4 | "blogDescription": "Nyheter, uppdateringar och meddelanden om Rallly." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/th/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "โพสต์ล่าสุด", 3 | "blogTitle": "Rallly - บล็อก", 4 | "blogDescription": "ข่าว, อัปเดต และ ประกาศ เกี่ยวกับ Rallly" 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/th/home.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/th/pricing.json: -------------------------------------------------------------------------------- 1 | { 2 | "pricing": "แผนราคา" 3 | } 4 | -------------------------------------------------------------------------------- /apps/landing/public/locales/tr/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "Son Gönderiler", 3 | "blogTitle": "Rallly - Blog", 4 | "blogDescription": "Rallly hakkında, haberler, güncellemeler ve duyurular." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/vi/blog.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/vi/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "Đăng nhập", 3 | "links": "Liên kết", 4 | "blog": "Blog", 5 | "discussions": "Thảo luận", 6 | "footerCredit": "Làm bởi imlukevella", 7 | "footerSponsor": "Dự án này do người dùng tài trợ. Vui lòng xem xét hỗ trợ nó bằng cách đóng góp.", 8 | "language": "Ngôn ngữ", 9 | "poweredBy": "Chạy trên nền tảng", 10 | "privacyPolicy": "Chính sách bảo mật", 11 | "support": "Hỗ trợ", 12 | "volunteerTranslator": "Giúp dịch trang này", 13 | "notFoundTitle": "Không tìm thấy trang 404", 14 | "notFoundDescription": "Chúng tôi không thấy trang bạn đang tìm.", 15 | "goToHome": "Về trang chủ", 16 | "getStarted": "Bắt đầu" 17 | } 18 | -------------------------------------------------------------------------------- /apps/landing/public/locales/vi/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "noLoginRequired": "Không cần đăng nhập", 3 | "new": "Thêm", 4 | "metaDescription": "Tạo bình chọn và bầu giờ hoặc ngày phù hợp nhất với cả nhóm." 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/vi/pricing.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/landing/public/locales/zh-Hant/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "最新貼文", 3 | "blogTitle": "Rallly - 部落格", 4 | "blogDescription": "關於 Rallly 的新聞、更新與公告。" 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/locales/zh-Hant/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "登入", 3 | "links": "一覽", 4 | "blog": "部落格", 5 | "discussions": "討論區", 6 | "footerCredit": "由 @imlukevella 製作", 7 | "footerSponsor": "此專案由用戶自治,請考慮透過 捐贈 支持本專案。", 8 | "language": "語言", 9 | "poweredBy": "技術支持", 10 | "privacyPolicy": "隱私權政策", 11 | "support": "技術支援", 12 | "cookiePolicy": "Cookie政策", 13 | "termsOfUse": "使用條款", 14 | "volunteerTranslator": "協助我們翻譯這個網站", 15 | "notFoundTitle": "404:頁面不存在", 16 | "notFoundDescription": "我們無法找到您要查找的頁面。", 17 | "goToHome": "回到首頁", 18 | "pricing": "定價", 19 | "bestDoodleAlternative": "最好的 Doodle 替代品", 20 | "freeSchedulingPoll": "免費的日程投票", 21 | "getStarted": "開始使用", 22 | "availabilityPoll": "日程投票", 23 | "solutions": "解決方案", 24 | "howItWorks": "Rallly的運作方式", 25 | "status": "狀態" 26 | } 27 | -------------------------------------------------------------------------------- /apps/landing/public/locales/zh/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "recentPosts": "最新文章", 3 | "blogTitle": "Rallly - 博客", 4 | "blogDescription": "有关 Rallly 的新闻、更新和公告。" 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/logo.png -------------------------------------------------------------------------------- /apps/landing/public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/mstile-144x144.png -------------------------------------------------------------------------------- /apps/landing/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/mstile-150x150.png -------------------------------------------------------------------------------- /apps/landing/public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/mstile-310x150.png -------------------------------------------------------------------------------- /apps/landing/public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/mstile-310x310.png -------------------------------------------------------------------------------- /apps/landing/public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/mstile-70x70.png -------------------------------------------------------------------------------- /apps/landing/public/og-image-1200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/og-image-1200.png -------------------------------------------------------------------------------- /apps/landing/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /api 3 | Disallow: /locales 4 | Allow: /api/og-image 5 | Sitemap: https://rallly.co/sitemap.xml -------------------------------------------------------------------------------- /apps/landing/public/static/fonts/inter-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/fonts/inter-bold.ttf -------------------------------------------------------------------------------- /apps/landing/public/static/fonts/inter-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/fonts/inter-regular.ttf -------------------------------------------------------------------------------- /apps/landing/public/static/images/animations.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/animations.gif -------------------------------------------------------------------------------- /apps/landing/public/static/images/device-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/device-data.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/eric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/eric.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/google.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/grouped-times.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/grouped-times.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/hero-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/hero-shot.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/hero.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/introducing-rallly-3-0/navigation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/introducing-rallly-3-0/navigation.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/introducing-rallly-3-0/poll-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/introducing-rallly-3-0/poll-status.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/introducing-rallly-3-0/polls-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/introducing-rallly-3-0/polls-page.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/introducing-rallly-3-0/updated-scoring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/introducing-rallly-3-0/updated-scoring.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/introducing-rallly-pro/finalize-poll-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/introducing-rallly-pro/finalize-poll-demo.gif -------------------------------------------------------------------------------- /apps/landing/public/static/images/introducing-rallly-pro/finalize-poll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/introducing-rallly-pro/finalize-poll.gif -------------------------------------------------------------------------------- /apps/landing/public/static/images/introducing-rallly-pro/rallly-pro-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/introducing-rallly-pro/rallly-pro-splash.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/luke-vella.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/luke-vella.jpg -------------------------------------------------------------------------------- /apps/landing/public/static/images/mobile-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/mobile-demo.gif -------------------------------------------------------------------------------- /apps/landing/public/static/images/pcmag-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/landing/public/static/images/rallly-pro-launch/billing-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/rallly-pro-launch/billing-page.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/rallly-pro-launch/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/rallly-pro-launch/email.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/rallly-pro-launch/finalize-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/rallly-pro-launch/finalize-page.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/share-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/share-demo.gif -------------------------------------------------------------------------------- /apps/landing/public/static/images/shortcut.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/shortcut.gif -------------------------------------------------------------------------------- /apps/landing/public/static/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/splash.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/timeslots-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/timeslots-demo.gif -------------------------------------------------------------------------------- /apps/landing/public/static/images/touchable-area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/touchable-area.png -------------------------------------------------------------------------------- /apps/landing/public/static/images/twitter-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/landing/public/static/images/twitter-card.png -------------------------------------------------------------------------------- /apps/landing/public/static/scripts/mailerlite.js: -------------------------------------------------------------------------------- 1 | (function (w, d, e, u, f, l, n) { 2 | (w[f] = 3 | w[f] || 4 | function () { 5 | (w[f].q = w[f].q || []).push(arguments); 6 | }), 7 | (l = d.createElement(e)), 8 | (l.async = 1), 9 | (l.src = u), 10 | (n = d.getElementsByTagName(e)[0]), 11 | n.parentNode.insertBefore(l, n); 12 | })( 13 | window, 14 | document, 15 | "script", 16 | "https://assets.mailerlite.com/js/universal.js", 17 | "ml", 18 | ); 19 | ml("account", "99567"); 20 | -------------------------------------------------------------------------------- /apps/landing/src/app/[locale]/(main)/[...notFound]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | 3 | export default function CatchAllPage() { 4 | notFound(); 5 | } 6 | -------------------------------------------------------------------------------- /apps/landing/src/app/[locale]/(main)/nav-link.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@rallly/ui"; 4 | import { Button } from "@rallly/ui/button"; 5 | import Link from "next/link"; 6 | import { usePathname } from "next/navigation"; 7 | 8 | export const NavLink = ({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) => { 12 | const pathname = usePathname(); 13 | const isActive = pathname === props.href; 14 | return ( 15 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/landing/src/app/[locale]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import ErrorPage from "@/components/error-page"; 2 | import { getTranslation } from "@/i18n/server"; 3 | 4 | export default async function Page() { 5 | // TODO (Luke Vella) [2023-11-03]: not-found doesn't have access to params right now 6 | // See: https://github.com/vercel/next.js/discussions/43179 7 | const { t } = await getTranslation("en"); 8 | 9 | return ( 10 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/landing/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /apps/landing/src/assets/linkedin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /apps/landing/src/assets/twitter.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | -------------------------------------------------------------------------------- /apps/landing/src/components/blog/date-formatter.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import localizedFormat from "dayjs/plugin/localizedFormat"; 3 | 4 | dayjs.extend(localizedFormat); 5 | 6 | type Props = { 7 | dateString: string; 8 | }; 9 | 10 | const DateFormatter = ({ dateString }: Props) => { 11 | return ; 12 | }; 13 | 14 | export default DateFormatter; 15 | -------------------------------------------------------------------------------- /apps/landing/src/components/blog/post-body.tsx: -------------------------------------------------------------------------------- 1 | import markdownStyles from "./markdown-styles.module.css"; 2 | 3 | type Props = { 4 | content: string; 5 | }; 6 | 7 | const PostBody = ({ content }: Props) => { 8 | return ( 9 |
14 | ); 15 | }; 16 | 17 | export default PostBody; 18 | -------------------------------------------------------------------------------- /apps/landing/src/components/blog/post-header.tsx: -------------------------------------------------------------------------------- 1 | import DateFormatter from "./date-formatter"; 2 | 3 | type Props = { 4 | title: string; 5 | date: string; 6 | }; 7 | 8 | const PostHeader = ({ title, date }: Props) => { 9 | return ( 10 |
11 |

12 | {title} 13 |

14 |
15 | 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default PostHeader; 22 | -------------------------------------------------------------------------------- /apps/landing/src/components/home/scribble-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/landing/src/fonts/handwritten.ts: -------------------------------------------------------------------------------- 1 | import { Playpen_Sans } from "next/font/google"; 2 | 3 | export const handwritten = Playpen_Sans({ 4 | subsets: ["latin"], 5 | display: "swap", 6 | }); 7 | -------------------------------------------------------------------------------- /apps/landing/src/fonts/sans.ts: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | 3 | export const sans = Inter({ 4 | subsets: ["latin"], 5 | display: "swap", 6 | }); 7 | -------------------------------------------------------------------------------- /apps/landing/src/i18n/client/trans.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Trans as BaseTrans, useTranslation } from "react-i18next"; 3 | 4 | type TransWithContextProps = Omit, "t">; 5 | 6 | export const Trans = (props: TransWithContextProps) => { 7 | const { t } = useTranslation(props.ns); 8 | return ; 9 | }; 10 | -------------------------------------------------------------------------------- /apps/landing/src/i18n/client/use-translation.ts: -------------------------------------------------------------------------------- 1 | import type { Namespace } from "i18next"; 2 | import { useTranslation as useTranslationOrg } from "react-i18next"; 3 | 4 | export function useTranslation(ns?: Namespace) { 5 | return useTranslationOrg(ns); 6 | } 7 | -------------------------------------------------------------------------------- /apps/landing/src/i18n/settings.ts: -------------------------------------------------------------------------------- 1 | import allLanguages from "@rallly/languages"; 2 | import type { InitOptions, Namespace } from "i18next"; 3 | 4 | export const fallbackLng = "en"; 5 | export const languages = Object.keys(allLanguages); 6 | export const defaultNS = "common"; 7 | 8 | export function getOptions( 9 | lng = fallbackLng, 10 | ns: Namespace = defaultNS, 11 | ): InitOptions { 12 | return { 13 | // debug: true, 14 | supportedLngs: languages, 15 | fallbackLng, 16 | lng, 17 | fallbackNS: defaultNS, 18 | defaultNS, 19 | ns, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /apps/landing/src/lib/linkToApp.ts: -------------------------------------------------------------------------------- 1 | export const linkToApp = (path = "") => { 2 | const url = new URL(path, process.env.NEXT_PUBLIC_APP_BASE_URL); 3 | return url.href; 4 | }; 5 | -------------------------------------------------------------------------------- /apps/landing/src/types.ts: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | 3 | import type { JSX } from "react"; 4 | 5 | export type ReactTag = keyof JSX.IntrinsicElements; 6 | 7 | export type PropsOf = TTag extends React.ElementType 8 | ? React.ComponentProps 9 | : never; 10 | 11 | export type Post = { 12 | slug: string; 13 | title: string; 14 | date: string; 15 | coverImage?: string; 16 | excerpt: string; 17 | content: string; 18 | }; 19 | -------------------------------------------------------------------------------- /apps/landing/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require("@rallly/tailwind-config/tailwind.config"); 2 | 3 | module.exports = sharedConfig; 4 | -------------------------------------------------------------------------------- /apps/landing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rallly/tsconfig/next.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"], 7 | "~/*": ["public/*"] 8 | }, 9 | "checkJs": false, 10 | "strictNullChecks": true, 11 | "target": "ES2017" 12 | }, 13 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 14 | "exclude": ["node_modules", ".next"] 15 | } 16 | -------------------------------------------------------------------------------- /apps/landing/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "installCommand": "pnpm install", 3 | "buildCommand": "cd ../.. && pnpm db:generate && pnpm build:landing", 4 | "outputDirectory": ".next" 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/.env.test: -------------------------------------------------------------------------------- 1 | PORT=3002 2 | NEXT_PUBLIC_BASE_URL=http://localhost:3002 3 | AUTH_URL=$NEXT_PUBLIC_BASE_URL 4 | SECRET_PASSWORD=abcdef1234567890abcdef1234567890 5 | DATABASE_URL=postgres://postgres:postgres@localhost:5450/rallly 6 | SUPPORT_EMAIL=support@rallly.co 7 | SMTP_HOST=0.0.0.0 8 | SMTP_PORT=1025 9 | QUICK_CREATE_ENABLED=true 10 | API_SECRET=1234567890abcdef1234567890abcdef1234 11 | INITIAL_ADMIN_EMAIL=initial.admin@rallly.co 12 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Sentry 4 | .sentryclirc 5 | 6 | # playwright 7 | /playwright-report 8 | /test-results 9 | # Sentry Config File 10 | .env.sentry-build-plugin 11 | -------------------------------------------------------------------------------- /apps/web/declarations/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import "i18next"; 2 | 3 | import type emails from "@rallly/emails/locales/emails.json"; 4 | 5 | import type app from "../public/locales/en/app.json"; 6 | 7 | declare module "i18next" { 8 | interface CustomTypeOptions { 9 | defaultNS: "app"; 10 | returnNull: false; 11 | resources: { 12 | app: typeof app; 13 | emails: typeof emails; 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/i18n.config.js: -------------------------------------------------------------------------------- 1 | const languages = require("@rallly/languages/languages.json"); 2 | 3 | module.exports = { 4 | defaultLocale: "en", 5 | locales: Object.keys(languages), 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/i18next-scanner.config.js: -------------------------------------------------------------------------------- 1 | const typescriptTransform = require("i18next-scanner-typescript"); 2 | 3 | module.exports = { 4 | input: ["src/**/*.{ts,tsx}", "!src/next-auth*.ts"], 5 | options: { 6 | nsSeparator: false, 7 | defaultNs: "app", 8 | defaultValue: "__STRING_NOT_TRANSLATED__", 9 | lngs: ["en"], 10 | ns: ["app"], 11 | plural: false, 12 | removeUnusedKeys: true, 13 | func: { 14 | list: ["t"], 15 | }, 16 | resource: { 17 | loadPath: "public/locales/{{lng}}/{{ns}}.json", 18 | savePath: "public/locales/{{lng}}/{{ns}}.json", 19 | }, 20 | }, 21 | format: "json", 22 | fallbackLng: "en", 23 | transform: typescriptTransform(), 24 | }; 25 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/web/next-i18next.config.js: -------------------------------------------------------------------------------- 1 | const ICU = require("i18next-icu/i18nextICU.js"); 2 | const path = require("node:path"); 3 | const i18n = require("./i18n.config.js"); 4 | 5 | module.exports = { 6 | i18n, 7 | defaultNS: "app", 8 | reloadOnPrerender: process.env.NODE_ENV === "development", 9 | localePath: path.resolve("./public/locales"), 10 | use: [new ICU()], 11 | serializeConfig: false, 12 | }; 13 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/web/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-167x167.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /apps/web/public/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/favicon-128x128.png -------------------------------------------------------------------------------- /apps/web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/favicon-16x16.png -------------------------------------------------------------------------------- /apps/web/public/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/favicon-196x196.png -------------------------------------------------------------------------------- /apps/web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/favicon-32x32.png -------------------------------------------------------------------------------- /apps/web/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/favicon-96x96.png -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/images/rallly-logo-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/images/rallly-logo-mark.png -------------------------------------------------------------------------------- /apps/web/public/locales/nb/app.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/web/public/locales/th/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "ลงชื่อเข้าใช้", 3 | "common_language": "ภาษา", 4 | "common_support": "ช่วยเหลือ", 5 | "errors_notFoundTitle": "404 ไม่พบสิ่งที่ค้นหา", 6 | "errors_notFoundDescription": "เราไม่พบหน้าที่คุณค้นหา", 7 | "errors_goToHome": "ไปยังหน้าหลัก", 8 | "language": "ภาษา", 9 | "support": "ช่วยเหลือ", 10 | "pricing": "แผนราคา" 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/logo.png -------------------------------------------------------------------------------- /apps/web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Rallly", 3 | "name": "Rallly", 4 | "icons": [ 5 | { 6 | "src": "android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/", 17 | "scope": "/", 18 | "display": "standalone", 19 | "background_color": "#F3F4F6", 20 | "theme_color": "#4F46E5" 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/mstile-144x144.png -------------------------------------------------------------------------------- /apps/web/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/mstile-150x150.png -------------------------------------------------------------------------------- /apps/web/public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/mstile-310x150.png -------------------------------------------------------------------------------- /apps/web/public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/mstile-310x310.png -------------------------------------------------------------------------------- /apps/web/public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/mstile-70x70.png -------------------------------------------------------------------------------- /apps/web/public/og-image-1200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/og-image-1200.png -------------------------------------------------------------------------------- /apps/web/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /api 3 | Disallow: /locales 4 | Allow: /api/og-image-poll 5 | -------------------------------------------------------------------------------- /apps/web/public/static/fonts/inter-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/static/fonts/inter-bold.ttf -------------------------------------------------------------------------------- /apps/web/public/static/fonts/inter-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/public/static/fonts/inter-regular.ttf -------------------------------------------------------------------------------- /apps/web/public/static/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/static/microsoft.svg: -------------------------------------------------------------------------------- 1 | MS-SymbolLockup -------------------------------------------------------------------------------- /apps/web/public/static/yahoo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; 9 | 10 | Sentry.init({ 11 | dsn: SENTRY_DSN, 12 | // Adjust this value in production, or use tracesSampler for greater control 13 | tracesSampleRate: 0.2, 14 | 15 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 16 | debug: false, 17 | }); 18 | -------------------------------------------------------------------------------- /apps/web/sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; 8 | 9 | Sentry.init({ 10 | dsn: SENTRY_DSN, 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 0.2, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | 17 | // Uncomment the line below to enable Spotlight (https://spotlightjs.com) 18 | // spotlight: process.env.NODE_ENV === 'development', 19 | }); 20 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(auth)/login/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | export async function setVerificationEmail(email: string) { 6 | const cookieStore = await cookies(); 7 | 8 | cookieStore.set("verification-email", email, { 9 | httpOnly: true, 10 | secure: process.env.NEXT_PUBLIC_BASE_URL?.startsWith("https://"), 11 | sameSite: "lax", 12 | maxAge: 15 * 60, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(auth)/login/components/auth-errors.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useSearchParams } from "next/navigation"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | export function AuthErrors() { 6 | const { t } = useTranslation(); 7 | const searchParams = useSearchParams(); 8 | const error = searchParams?.get("error"); 9 | if (error === "OAuthAccountNotLinked") { 10 | return ( 11 |

12 | {t("accountNotLinkedDescription", { 13 | defaultValue: 14 | "A user with this email already exists. Please log in using the original method.", 15 | })} 16 |

17 | ); 18 | } 19 | 20 | return null; 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(auth)/login/components/login-with-oidc.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@rallly/ui/button"; 3 | import { signIn } from "next-auth/react"; 4 | 5 | import { Trans } from "@/components/trans"; 6 | 7 | export function LoginWithOIDC({ 8 | name, 9 | redirectTo, 10 | }: { 11 | name: string; 12 | redirectTo?: string; 13 | }) { 14 | return ( 15 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(auth)/login/components/or-divider.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslation } from "@/i18n/server"; 2 | 3 | export async function OrDivider() { 4 | const { t } = await getTranslation(); 5 | return ( 6 |
7 |
8 |
{t("or")}
9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(auth)/login/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from "@/components/spinner"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(auth)/register/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | export async function setToken(token: string) { 6 | (await cookies()).set("registration-token", token, { 7 | httpOnly: true, 8 | secure: process.env.NEXT_PUBLIC_BASE_URL?.startsWith("https://"), 9 | sameSite: "lax", 10 | maxAge: 15 * 60, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(auth)/register/components/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { isValidName } from "@/utils/is-valid-name"; 4 | 5 | export const registerNameFormSchema = z.object({ 6 | name: z.string().trim().min(1).max(100).refine(isValidName, { 7 | message: "Please enter a valid name, not a URL, email, or phone number", 8 | }), 9 | email: z.string().email(), 10 | }); 11 | 12 | export type RegisterNameFormValues = z.infer; 13 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(auth)/register/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from "@/components/spinner"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/admin-setup/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { isInitialAdmin, requireUser } from "@/auth/queries"; 4 | import { prisma } from "@rallly/database"; 5 | 6 | export async function makeAdmin() { 7 | const user = await requireUser(); 8 | 9 | if (!isInitialAdmin(user.email)) { 10 | return { success: false, error: "Unauthorized" }; 11 | } 12 | 13 | await prisma.user.update({ 14 | where: { 15 | id: user.id, 16 | }, 17 | data: { 18 | role: "admin", 19 | }, 20 | }); 21 | 22 | return { success: true, error: null }; 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/components/sidebar/app-sidebar-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SidebarProvider } from "@rallly/ui/sidebar"; 4 | import { useLocalStorage } from "react-use"; 5 | 6 | export function AppSidebarProvider({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | const [value, setValue] = useLocalStorage("sidebar_state", "expanded"); 12 | return ( 13 | { 16 | setValue(open ? "expanded" : "collapsed"); 17 | }} 18 | > 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/components/sidebar/nav-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SidebarMenuButton, SidebarMenuItem } from "@rallly/ui/sidebar"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | export function NavItem({ 8 | href, 9 | children, 10 | }: { 11 | href: string; 12 | children: React.ReactNode; 13 | }) { 14 | const pathname = usePathname(); 15 | const isActive = pathname === href; 16 | 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/components/upgrade-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DialogTrigger } from "@rallly/ui/dialog"; 4 | 5 | import { PayWallDialog } from "@/components/pay-wall-dialog"; 6 | 7 | export function UpgradeButton({ children }: React.PropsWithChildren) { 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/events/types.ts: -------------------------------------------------------------------------------- 1 | export type ScheduledEvent = { 2 | id: string; 3 | title: string; 4 | start: Date; 5 | duration: number; 6 | timeZone: string | null; 7 | participants: { name: string }[]; 8 | }; 9 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/loading.tsx: -------------------------------------------------------------------------------- 1 | import { PageSkeleton } from "@/app/components/page-layout"; 2 | import { RouterLoadingIndicator } from "@/components/router-loading-indicator"; 3 | 4 | export default async function Loading() { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/settings/components/sign-out-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@rallly/ui/button"; 4 | import { Icon } from "@rallly/ui/icon"; 5 | import { LogOutIcon } from "lucide-react"; 6 | import { signOut } from "next-auth/react"; 7 | 8 | import { Trans } from "@/components/trans"; 9 | 10 | export const SignOutButton = () => { 11 | return ( 12 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | import { RouterLoadingIndicator } from "@/components/router-loading-indicator"; 2 | import { Spinner } from "@/components/spinner"; 3 | 4 | export default async function Loading() { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/settings/preferences/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { prisma } from "@rallly/database"; 4 | 5 | import { getUserId } from "@/next-auth"; 6 | 7 | export async function updateLocale(locale: string) { 8 | const userId = await getUserId(); 9 | await prisma.user.update({ 10 | where: { 11 | id: userId, 12 | }, 13 | data: { 14 | locale, 15 | }, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/settings/preferences/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from "@/components/spinner"; 2 | 3 | export default async function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/(space)/settings/preferences/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Params } from "@/app/[locale]/types"; 2 | import { getTranslation } from "@/i18n/server"; 3 | 4 | import { PreferencesPage } from "./preferences-page"; 5 | 6 | export default async function Page() { 7 | return ; 8 | } 9 | 10 | export async function generateMetadata(props: { params: Promise }) { 11 | const params = await props.params; 12 | const { t } = await getTranslation(params.locale); 13 | return { 14 | title: t("preferences"), 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/[...notFound]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | 3 | export default function CatchAllPage() { 4 | notFound(); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/admin/[adminUrlId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { prisma } from "@rallly/database"; 2 | import { absoluteUrl } from "@rallly/utils/absolute-url"; 3 | import { notFound } from "next/navigation"; 4 | 5 | import { Redirect } from "@/app/components/redirect"; 6 | 7 | import type { PParams } from "./types"; 8 | 9 | export default async function Page(props: { params: Promise }) { 10 | const params = await props.params; 11 | const { adminUrlId } = params; 12 | 13 | const poll = await prisma.poll.findUnique({ 14 | where: { adminUrlId }, 15 | select: { id: true }, 16 | }); 17 | 18 | if (!poll) { 19 | notFound(); 20 | } 21 | 22 | return ( 23 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/admin/[adminUrlId]/types.ts: -------------------------------------------------------------------------------- 1 | import type { Params } from "@/app/[locale]/types"; 2 | 3 | export interface PParams extends Params { 4 | adminUrlId: string; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/control-panel/control-panel-sidebar-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SidebarProvider } from "@rallly/ui/sidebar"; 4 | import { useLocalStorage } from "react-use"; 5 | 6 | export function ControlPanelSidebarProvider({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | const [value, setValue] = useLocalStorage( 12 | "control-panel-sidebar_state", 13 | "expanded", 14 | ); 15 | return ( 16 | { 19 | setValue(open ? "expanded" : "collapsed"); 20 | }} 21 | > 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/control-panel/nav-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SidebarMenuButton, SidebarMenuItem } from "@rallly/ui/sidebar"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | export function NavItem({ 8 | href, 9 | children, 10 | }: { 11 | href: string; 12 | children: React.ReactNode; 13 | }) { 14 | const pathname = usePathname(); 15 | const isActive = pathname === href; 16 | 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/control-panel/users/user-search-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SearchInput } from "@/app/components/search-input"; 4 | import { useTranslation } from "@/i18n/client"; 5 | 6 | export function UserSearchInput() { 7 | const { t } = useTranslation(); 8 | return ( 9 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/invite/[urlId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from "@/components/spinner"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/invite/[urlId]/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider"; 4 | import { VisibilityProvider } from "@/components/visibility"; 5 | 6 | export default function Providers({ children }: { children: React.ReactNode }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/new/back-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@rallly/ui/button"; 4 | import { Icon } from "@rallly/ui/icon"; 5 | import { ArrowLeftIcon } from "lucide-react"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | import { Trans } from "@/components/trans"; 9 | 10 | export function BackButton() { 11 | const router = useRouter(); 12 | 13 | return ( 14 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/new/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/p/[participantUrlId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { prisma } from "@rallly/database"; 2 | import { absoluteUrl } from "@rallly/utils/absolute-url"; 3 | import { notFound } from "next/navigation"; 4 | 5 | import { Redirect } from "@/app/components/redirect"; 6 | 7 | import type { PParams } from "./types"; 8 | 9 | export default async function Page(props: { params: Promise }) { 10 | const params = await props.params; 11 | const { participantUrlId } = params; 12 | 13 | const poll = await prisma.poll.findUnique({ 14 | where: { participantUrlId }, 15 | select: { id: true }, 16 | }); 17 | 18 | if (!poll) { 19 | notFound(); 20 | } 21 | 22 | return ( 23 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/p/[participantUrlId]/types.ts: -------------------------------------------------------------------------------- 1 | import type { Params } from "@/app/[locale]/types"; 2 | 3 | export interface PParams extends Params { 4 | participantUrlId: string; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/poll/[urlId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from "@/components/spinner"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/poll/[urlId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { AdminPage } from "@/app/[locale]/poll/[urlId]/admin-page"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/app/[locale]/types.ts: -------------------------------------------------------------------------------- 1 | export interface Params { 2 | locale: string; 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { rateLimit } from "@/features/rate-limit"; 2 | import { handlers } from "@/next-auth"; 3 | import { withPosthog } from "@/utils/posthog"; 4 | import type { NextRequest } from "next/server"; 5 | 6 | export const GET = withPosthog(async (req: NextRequest) => { 7 | if (req.nextUrl.pathname.includes("callback/email")) { 8 | const { success } = await rateLimit("login_otp_attempt", 20, "15m"); 9 | 10 | if (!success) { 11 | return new Response("Too many requests", { 12 | status: 429, 13 | }); 14 | } 15 | } 16 | 17 | return handlers.GET(req); 18 | }); 19 | 20 | export const POST = withPosthog(handlers.POST); 21 | -------------------------------------------------------------------------------- /apps/web/src/app/api/auth/invalid-session/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import { signOut } from "@/next-auth"; 5 | 6 | export async function GET(req: NextRequest) { 7 | await signOut({ 8 | redirect: false, 9 | }); 10 | 11 | return NextResponse.redirect(new URL("/login", req.url)); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/app/api/status/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@rallly/database"; 2 | import { NextResponse } from "next/server"; 3 | 4 | async function getDatabaseStatus() { 5 | try { 6 | await prisma.$connect(); 7 | return "connected"; 8 | } catch { 9 | return "disconnected"; 10 | } 11 | } 12 | 13 | export const GET = async () => { 14 | const database = await getDatabaseStatus(); 15 | const version = process.env.NEXT_PUBLIC_APP_VERSION || "unknown"; 16 | const environment = process.env.NODE_ENV; 17 | const timestamp = new Date().toISOString(); 18 | 19 | const status = { 20 | status: "ok", 21 | timestamp, 22 | version, 23 | environment, 24 | database, 25 | }; 26 | 27 | return NextResponse.json(status); 28 | }; 29 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/portal/payment-methods/route.ts: -------------------------------------------------------------------------------- 1 | import { createStripePortalSessionHandler } from "../helpers/create-portal-session"; 2 | 3 | export const GET = createStripePortalSessionHandler("/payment-methods"); 4 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/portal/route.ts: -------------------------------------------------------------------------------- 1 | import { createStripePortalSessionHandler } from "./helpers/create-portal-session"; 2 | 3 | export const GET = createStripePortalSessionHandler(); 4 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/webhook/handlers/checkout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./completed"; 2 | export * from "./expired"; 3 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./created"; 2 | export * from "./deleted"; 3 | export * from "./updated"; 4 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/webhook/handlers/customer/created.ts: -------------------------------------------------------------------------------- 1 | import type { Stripe } from "@rallly/billing"; 2 | import { prisma } from "@rallly/database"; 3 | import { z } from "zod"; 4 | 5 | const customerMetadataSchema = z.object({ 6 | userId: z.string(), 7 | }); 8 | 9 | export async function onCustomerCreated(event: Stripe.Event) { 10 | const customer = event.data.object as Stripe.Customer; 11 | 12 | const res = customerMetadataSchema.safeParse(customer.metadata); 13 | 14 | if (!res.success) { 15 | // If there's no userId in metadata, ignore the event 16 | return; 17 | } 18 | 19 | const { userId } = res.data; 20 | 21 | // Update the user with the customer id 22 | await prisma.user.update({ 23 | where: { 24 | id: userId, 25 | }, 26 | data: { 27 | customerId: customer.id, 28 | }, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/webhook/handlers/customer/deleted.ts: -------------------------------------------------------------------------------- 1 | import type { Stripe } from "@rallly/billing"; 2 | import { prisma } from "@rallly/database"; 3 | 4 | export async function onCustomerDeleted(event: Stripe.Event) { 5 | const customer = event.data.object as Stripe.Customer; 6 | 7 | // Find and update the user with this customerId 8 | await prisma.user.updateMany({ 9 | where: { 10 | customerId: customer.id, 11 | }, 12 | data: { 13 | customerId: null, 14 | }, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/webhook/handlers/customer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./created"; 2 | export * from "./deleted"; 3 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/webhook/handlers/payment-method/detached.ts: -------------------------------------------------------------------------------- 1 | import type { Stripe } from "@rallly/billing"; 2 | import { prisma } from "@rallly/database"; 3 | 4 | export async function onPaymentMethodDetached(event: Stripe.Event) { 5 | const paymentMethod = event.data.object as Stripe.PaymentMethod; 6 | 7 | // Delete the payment method from our database 8 | await prisma.paymentMethod.delete({ 9 | where: { 10 | id: paymentMethod.id, 11 | }, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/webhook/handlers/payment-method/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./attached"; 2 | export * from "./detached"; 3 | export * from "./updated"; 4 | -------------------------------------------------------------------------------- /apps/web/src/app/api/stripe/webhook/handlers/payment-method/updated.ts: -------------------------------------------------------------------------------- 1 | import type { Stripe } from "@rallly/billing"; 2 | import { type Prisma, prisma } from "@rallly/database"; 3 | 4 | export async function onPaymentMethodUpdated(event: Stripe.Event) { 5 | const paymentMethod = event.data.object as Stripe.PaymentMethod; 6 | 7 | if (!paymentMethod.customer) { 8 | return; 9 | } 10 | 11 | // Update the payment method data in our database 12 | await prisma.paymentMethod.update({ 13 | where: { 14 | id: paymentMethod.id, 15 | }, 16 | data: { 17 | type: paymentMethod.type, 18 | data: paymentMethod[paymentMethod.type] as Prisma.JsonObject, 19 | }, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/app/components/logo-link.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | export function LogoLink() { 5 | return ( 6 | 10 | Rallly 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/app/components/logout-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import type { ButtonProps } from "@rallly/ui/button"; 3 | import { Button } from "@rallly/ui/button"; 4 | 5 | import { useUser } from "@/components/user-provider"; 6 | 7 | export function LogoutButton({ 8 | children, 9 | onClick, 10 | ...rest 11 | }: React.PropsWithChildren) { 12 | const { logout } = useUser(); 13 | return ( 14 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Sentry from "@sentry/nextjs"; 4 | import NextError from "next/error"; 5 | import { useEffect } from "react"; 6 | 7 | export default function GlobalError({ 8 | error, 9 | }: { 10 | error: Error & { digest?: string }; 11 | }) { 12 | useEffect(() => { 13 | Sentry.captureException(error); 14 | }, [error]); 15 | 16 | return ( 17 | 18 | 19 | {/* `NextError` is the default Next.js error page component. Its type 20 | definition requires a `statusCode` prop. However, since the App Router 21 | does not expose status codes for errors, we simply pass 0 to render a 22 | generic error message. */} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/src/app/posthog-page-view.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePostHog } from "@rallly/posthog/client"; 3 | import { usePathname, useSearchParams } from "next/navigation"; 4 | import { useEffect } from "react"; 5 | 6 | export function PostHogPageView() { 7 | const pathname = usePathname(); 8 | const searchParams = useSearchParams(); 9 | const posthog = usePostHog(); 10 | useEffect(() => { 11 | // Track pageviews 12 | if (pathname && posthog) { 13 | let url = window.origin + pathname; 14 | if (searchParams?.toString()) { 15 | url = `${url}?${searchParams.toString()}`; 16 | } 17 | posthog.capture("$pageview", { 18 | $current_url: url, 19 | }); 20 | } 21 | }, [pathname, searchParams, posthog]); 22 | 23 | return null; 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/auth/edge/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./with-auth"; 2 | -------------------------------------------------------------------------------- /apps/web/src/auth/helpers/get-optional-providers.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from "next-auth/providers/index"; 2 | 3 | import { GoogleProvider } from "../providers/google"; 4 | import { MicrosoftProvider } from "../providers/microsoft"; 5 | import { OIDCProvider } from "../providers/oidc"; 6 | 7 | export function getOptionalProviders() { 8 | return [OIDCProvider(), GoogleProvider(), MicrosoftProvider()].filter( 9 | Boolean, 10 | ) as Provider[]; 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/src/auth/providers/google.ts: -------------------------------------------------------------------------------- 1 | import BaseGoogleProvider from "next-auth/providers/google"; 2 | 3 | export function GoogleProvider() { 4 | if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { 5 | return BaseGoogleProvider({ 6 | clientId: process.env.GOOGLE_CLIENT_ID, 7 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 8 | allowDangerousEmailAccountLinking: true, 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/src/auth/providers/guest.ts: -------------------------------------------------------------------------------- 1 | import { randomid } from "@rallly/utils/nanoid"; 2 | import CredentialsProvider from "next-auth/providers/credentials"; 3 | 4 | export const GuestProvider = CredentialsProvider({ 5 | id: "guest", 6 | name: "Guest", 7 | credentials: {}, 8 | async authorize() { 9 | return { 10 | id: `user-${randomid()}`, 11 | email: null, 12 | }; 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /apps/web/src/auth/providers/microsoft.ts: -------------------------------------------------------------------------------- 1 | import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"; 2 | 3 | export function MicrosoftProvider() { 4 | if ( 5 | process.env.MICROSOFT_TENANT_ID && 6 | process.env.MICROSOFT_CLIENT_ID && 7 | process.env.MICROSOFT_CLIENT_SECRET 8 | ) { 9 | return MicrosoftEntraID({ 10 | name: "Microsoft", 11 | clientId: process.env.MICROSOFT_CLIENT_ID, 12 | clientSecret: process.env.MICROSOFT_CLIENT_SECRET, 13 | wellKnown: 14 | "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/src/components/container.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@rallly/ui"; 2 | 3 | export const Container = ({ 4 | children, 5 | className, 6 | }: React.PropsWithChildren<{ className?: string }>) => { 7 | return ( 8 |
{children}
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/src/components/discussion/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./discussion"; 2 | -------------------------------------------------------------------------------- /apps/web/src/components/forms/index.ts: -------------------------------------------------------------------------------- 1 | export type { PollDetailsData } from "./poll-details-form"; 2 | export { PollDetailsForm } from "./poll-details-form"; 3 | export type { PollOptionsData } from "./poll-options-form/poll-options-form"; 4 | export { default as PollOptionsForm } from "./poll-options-form/poll-options-form"; 5 | export * from "./types"; 6 | -------------------------------------------------------------------------------- /apps/web/src/components/forms/poll-options-form/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./poll-options-form"; 2 | export * from "./types"; 3 | -------------------------------------------------------------------------------- /apps/web/src/components/forms/poll-options-form/month-calendar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./month-calendar"; 2 | -------------------------------------------------------------------------------- /apps/web/src/components/forms/poll-options-form/types.ts: -------------------------------------------------------------------------------- 1 | export type DateOption = { 2 | type: "date"; 3 | date: string; 4 | }; 5 | 6 | export type TimeOption = { 7 | type: "timeSlot"; 8 | start: string; 9 | duration: number; 10 | end: string; 11 | }; 12 | 13 | export type DateTimeOption = DateOption | TimeOption; 14 | 15 | export interface DateTimePickerProps { 16 | title?: string; 17 | options: DateTimeOption[]; 18 | date?: Date; 19 | onNavigate: (date: Date) => void; 20 | onChange: (options: DateTimeOption[]) => void; 21 | duration: number; 22 | onChangeDuration: (duration: number) => void; 23 | scrollToTime?: Date; 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/components/forms/poll-options-form/utils.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | 3 | export const formatDateWithoutTz = (date: Date): string => { 4 | return dayjs(date).format("YYYY-MM-DDTHH:mm:ss"); 5 | }; 6 | 7 | export const formatDateWithoutTime = (date: Date): string => { 8 | return dayjs(date).format("YYYY-MM-DD"); 9 | }; 10 | -------------------------------------------------------------------------------- /apps/web/src/components/forms/types.ts: -------------------------------------------------------------------------------- 1 | import type { PollSettingsFormData } from "@/components/forms/poll-settings"; 2 | 3 | import type { PollDetailsData } from "./poll-details-form"; 4 | import type { PollOptionsData } from "./poll-options-form/poll-options-form"; 5 | 6 | export type NewEventData = PollDetailsData & 7 | PollOptionsData & 8 | PollSettingsFormData; 9 | 10 | // biome-ignore lint/suspicious/noExplicitAny: Fix this later 11 | export interface PollFormProps> { 12 | onSubmit?: (data: T) => void; 13 | onChange?: (data: Partial) => void; 14 | defaultValues?: Partial; 15 | name?: string; 16 | className?: string; 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/src/components/input-otp.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@rallly/ui/input"; 2 | import React from "react"; 3 | 4 | const InputOTP = React.forwardRef< 5 | HTMLInputElement, 6 | React.ComponentProps & { onValidCode?: (code: string) => void } 7 | >(({ onValidCode, onChange, ...rest }, ref) => { 8 | return ( 9 | { 13 | onChange?.(e); 14 | 15 | if (e.target.value.length === 6) { 16 | onValidCode?.(e.target.value); 17 | } 18 | }} 19 | maxLength={6} 20 | data-1p-ignore 21 | inputMode="numeric" 22 | autoComplete="one-time-code" 23 | pattern="\d{6}" 24 | /> 25 | ); 26 | }); 27 | 28 | InputOTP.displayName = "InputOTP"; 29 | 30 | export { InputOTP }; 31 | -------------------------------------------------------------------------------- /apps/web/src/components/login-link.tsx: -------------------------------------------------------------------------------- 1 | import type { LinkProps } from "next/link"; 2 | import Link from "next/link"; 3 | import { usePathname } from "next/navigation"; 4 | import React from "react"; 5 | 6 | export const LoginLink = React.forwardRef< 7 | HTMLAnchorElement, 8 | React.PropsWithChildren & { className?: string }> 9 | >(function LoginLink({ children, ...props }, ref) { 10 | const pathname = usePathname() ?? "/"; 11 | return ( 12 | 17 | {children} 18 | 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/web/src/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | const sizes = { 4 | sm: { 5 | width: 140, 6 | height: 22, 7 | }, 8 | md: { 9 | width: 150, 10 | height: 30, 11 | }, 12 | }; 13 | 14 | export const Logo = ({ 15 | className, 16 | size = "md", 17 | }: { 18 | className?: string; 19 | size?: keyof typeof sizes; 20 | }) => { 21 | return ( 22 | Rallly 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /apps/web/src/components/maintenance.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | const Maintenance: React.FunctionComponent = () => { 4 | return ( 5 |
6 | 7 | Down for maintenance - Be right back 8 | 9 |
10 |
11 | The site is currently down for some maintenance and will be back 12 | shortly… 13 |
14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Maintenance; 20 | -------------------------------------------------------------------------------- /apps/web/src/components/modal/index.ts: -------------------------------------------------------------------------------- 1 | export { useModal } from "./use-modal"; 2 | -------------------------------------------------------------------------------- /apps/web/src/components/modal/use-modal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { ModalProps } from "./modal"; 4 | import Modal from "./modal"; 5 | 6 | type OpenModalFn = () => void; 7 | type CloseModalFn = () => void; 8 | 9 | export const useModal = ( 10 | props?: ModalProps, 11 | ): [React.ReactElement, OpenModalFn, CloseModalFn] => { 12 | const [visible, setVisible] = React.useState(false); 13 | const modal = ( 14 | { 18 | props?.onOk?.(); 19 | setVisible(false); 20 | }} 21 | onCancel={() => { 22 | props?.onCancel?.(); 23 | setVisible(false); 24 | }} 25 | /> 26 | ); 27 | return [modal, () => setVisible(true), () => setVisible(false)]; 28 | }; 29 | -------------------------------------------------------------------------------- /apps/web/src/components/poll/guest-alert.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/apps/web/src/components/poll/guest-alert.tsx -------------------------------------------------------------------------------- /apps/web/src/components/poll/poll-context-provider.tsx: -------------------------------------------------------------------------------- 1 | import ModalProvider from "@/components/modal/modal-provider"; 2 | import { ParticipantsProvider } from "@/components/participants-provider"; 3 | import { 4 | OptionsProvider, 5 | PollContextProvider, 6 | } from "@/components/poll-context"; 7 | import { usePoll } from "@/contexts/poll"; 8 | 9 | export const LegacyPollContextProvider = (props: React.PropsWithChildren) => { 10 | const poll = usePoll(); 11 | 12 | if (!poll) { 13 | return null; 14 | } 15 | 16 | return ( 17 | 18 | 19 | 20 | {props.children} 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /apps/web/src/components/poll/poll-footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { Trans } from "@/components/trans"; 4 | 5 | export function PollFooter() { 6 | return ( 7 |
8 | 18 | ), 19 | }} 20 | /> 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/src/components/poll/poll-viz.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import DesktopPoll from "@/components/poll/desktop-poll"; 6 | import MobilePoll from "@/components/poll/mobile-poll"; 7 | 8 | const checkIfWideScreen = () => window.innerWidth > 640; 9 | 10 | export function PollViz() { 11 | React.useEffect(() => { 12 | const listener = () => setIsWideScreen(checkIfWideScreen()); 13 | 14 | window.addEventListener("resize", listener); 15 | 16 | return () => { 17 | window.removeEventListener("resize", listener); 18 | }; 19 | }, []); 20 | 21 | const [isWideScreen, setIsWideScreen] = React.useState(checkIfWideScreen); 22 | const PollComponent = isWideScreen ? DesktopPoll : MobilePoll; 23 | 24 | return ; 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/src/components/poll/responsive-results.tsx: -------------------------------------------------------------------------------- 1 | import { createBreakpoint } from "react-use"; 2 | 3 | import DesktopPoll from "@/components/poll/desktop-poll"; 4 | import MobilePoll from "@/components/poll/mobile-poll"; 5 | 6 | const useBreakpoint = createBreakpoint({ list: 320, table: 640 }); 7 | 8 | export function ResponsiveResults() { 9 | const breakpoint = useBreakpoint(); 10 | const PollComponent = breakpoint === "table" ? DesktopPoll : MobilePoll; 11 | 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/components/poll/types.ts: -------------------------------------------------------------------------------- 1 | import type { VoteType } from "@rallly/database"; 2 | 3 | export interface ParticipantForm { 4 | votes: Array< 5 | | { 6 | optionId: string; 7 | type?: VoteType; 8 | } 9 | | undefined 10 | >; 11 | } 12 | 13 | export interface ParticipantFormSubmitted { 14 | votes: Array<{ optionId: string; type: VoteType }>; 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/components/poll/you-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@rallly/ui/icon"; 2 | import { UserIcon } from "lucide-react"; 3 | 4 | export function YouAvatar() { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/components/pro-badge.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@rallly/ui/badge"; 2 | 3 | import { Trans } from "@/components/trans"; 4 | 5 | export const ProBadge = ({ className }: { className?: string }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/web/src/components/random-gradient-bar.tsx: -------------------------------------------------------------------------------- 1 | import { generateGradient } from "@/utils/color-hash"; 2 | 3 | export function RandomGradientBar({ seed }: { seed?: string }) { 4 | return ( 5 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/components/relative-date.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dayjs from "dayjs"; 3 | 4 | export function RelativeDate({ date }: { date: Date }) { 5 | return <>{dayjs(date).fromNow()}; 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/src/components/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@rallly/ui"; 2 | import type React from "react"; 3 | 4 | export const Skeleton = (props: { 5 | className?: string; 6 | children?: React.ReactNode; 7 | }) => { 8 | return ( 9 |
12 | {props.children} 13 |
14 | ); 15 | }; 16 | 17 | export function SkeletonCard({ 18 | children, 19 | className, 20 | }: { 21 | children?: React.ReactNode; 22 | className?: string; 23 | }) { 24 | return ( 25 |
26 | {children} 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/src/components/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@rallly/ui"; 2 | import { Loader2Icon } from "lucide-react"; 3 | 4 | export const Spinner = (props: { className?: string }) => { 5 | return ( 6 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/components/stacked-list.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@rallly/ui"; 2 | 3 | export function StackedList({ 4 | children, 5 | className, 6 | }: { 7 | children: React.ReactNode; 8 | className?: string; 9 | }) { 10 | return ( 11 |
    12 | {children} 13 |
14 | ); 15 | } 16 | 17 | export function StackedListItem({ 18 | children, 19 | className, 20 | }: { 21 | children: React.ReactNode; 22 | className?: string; 23 | }) { 24 | return ( 25 |
  • 31 | {children} 32 |
  • 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/src/components/steps/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./steps"; 2 | -------------------------------------------------------------------------------- /apps/web/src/components/steps/step.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | 3 | export interface StepProps { 4 | title: string; 5 | children?: React.ReactNode; 6 | } 7 | 8 | const Step: React.FunctionComponent = ({ title, children }) => { 9 | return ( 10 |
    11 |
    {title}
    12 |
    {children}
    13 |
    14 | ); 15 | }; 16 | 17 | export default Step; 18 | -------------------------------------------------------------------------------- /apps/web/src/components/trans.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Trans as BaseTrans, useTranslation } from "react-i18next"; 3 | 4 | import type { TxKeyPath } from "../i18n/types"; 5 | 6 | export const Trans = ( 7 | props: React.ComponentProps & { i18nKey: TxKeyPath }, 8 | ) => { 9 | const { t } = useTranslation(); 10 | return ; 11 | }; 12 | -------------------------------------------------------------------------------- /apps/web/src/components/use-required-context.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const useRequiredContext = ( 4 | context: React.Context, 5 | errorMessage?: string, 6 | ) => { 7 | const contextValue = React.useContext(context); 8 | if (contextValue === null) { 9 | throw new Error( 10 | errorMessage ?? `Missing context provider: ${context.displayName}`, 11 | ); 12 | } 13 | return contextValue; 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/src/contexts/environment.tsx: -------------------------------------------------------------------------------- 1 | export const isSelfHosted = process.env.NEXT_PUBLIC_SELF_HOSTED === "true"; 2 | 3 | export const IfSelfHosted = ({ children }: React.PropsWithChildren) => { 4 | return isSelfHosted ? <>{children} : null; 5 | }; 6 | 7 | export const IfCloudHosted = ({ children }: React.PropsWithChildren) => { 8 | return isSelfHosted ? null : <>{children}; 9 | }; 10 | -------------------------------------------------------------------------------- /apps/web/src/contexts/poll.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "next/navigation"; 2 | 3 | import { trpc } from "@/trpc/client"; 4 | 5 | export const usePoll = () => { 6 | const params = useParams<{ urlId: string }>(); 7 | const pollQuery = trpc.polls.get.useQuery({ urlId: params?.urlId as string }); 8 | 9 | if (!pollQuery.data) { 10 | throw new Error("Expected poll to be prefetched"); 11 | } 12 | 13 | return pollQuery.data; 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/src/contexts/role.tsx: -------------------------------------------------------------------------------- 1 | import { usePathname } from "next/navigation"; 2 | 3 | export const useRole = () => { 4 | const pathname = usePathname(); 5 | return pathname?.includes("/poll") ? "admin" : "participant"; 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/src/features/feedback/schema.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | export const feedbackSchema = z.object({ 4 | content: z.string().min(10).max(10000), 5 | }); 6 | 7 | export type Feedback = z.infer; 8 | -------------------------------------------------------------------------------- /apps/web/src/features/licensing/client.ts: -------------------------------------------------------------------------------- 1 | import { env } from "@/env"; 2 | import { LicensingClient } from "./lib/licensing-client"; 3 | 4 | export const licensingClient = new LicensingClient({ 5 | apiUrl: env.LICENSE_API_URL, 6 | authToken: env.LICENSE_API_AUTH_TOKEN, 7 | }); 8 | -------------------------------------------------------------------------------- /apps/web/src/features/licensing/helpers/calculate-checksum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculate a checksum for a string 3 | * @param str The string to calculate the checksum for 4 | * @returns The checksum 5 | */ 6 | export function calculateChecksum(str: string): string { 7 | // Simple checksum: sum char codes, mod 100000, base36 8 | let sum = 0; 9 | for (let i = 0; i < str.length; i++) { 10 | sum += str.charCodeAt(i); 11 | } 12 | return (sum % 100000).toString(36).toUpperCase().padStart(5, "0"); 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/features/licensing/helpers/check-license-key.ts: -------------------------------------------------------------------------------- 1 | import { calculateChecksum } from "./calculate-checksum"; 2 | 3 | export function checkLicenseKey(key: string): boolean { 4 | const parts = key.split("-"); 5 | if (parts.length !== 6) return false; 6 | const checksum = parts[5]; 7 | const licenseBody = parts.slice(0, 5).join("-"); 8 | return calculateChecksum(licenseBody) === checksum; 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/features/licensing/helpers/generate-license-key.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from "nanoid"; 2 | import { calculateChecksum } from "./calculate-checksum"; 3 | 4 | const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 5 | const generate = customAlphabet(alphabet, 4); 6 | 7 | /** 8 | * Generate user friendly licenses 9 | * eg. RLYV4-ABCD-1234-ABCD-1234-XXXX 10 | */ 11 | export const generateLicenseKey = ({ version }: { version?: number }) => { 12 | let license = `RLYV${version ?? "X"}-`; 13 | for (let i = 0; i < 4; i++) { 14 | license += generate(); 15 | if (i < 3) { 16 | license += "-"; 17 | } 18 | } 19 | const checksum = calculateChecksum(license); 20 | return `${license}-${checksum}`; 21 | }; 22 | -------------------------------------------------------------------------------- /apps/web/src/features/licensing/index.ts: -------------------------------------------------------------------------------- 1 | export { LicensingClient } from "./lib/licensing-client"; 2 | -------------------------------------------------------------------------------- /apps/web/src/features/licensing/mutations.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { requireAdmin } from "@/auth/queries"; 4 | import { prisma } from "@rallly/database"; 5 | 6 | export async function removeInstanceLicense({ 7 | licenseId, 8 | }: { 9 | licenseId: string; 10 | }) { 11 | try { 12 | await requireAdmin(); 13 | } catch (error) { 14 | return { 15 | success: false, 16 | message: "You must be an admin to delete a license", 17 | }; 18 | } 19 | 20 | try { 21 | await prisma.instanceLicense.delete({ 22 | where: { 23 | id: licenseId, 24 | }, 25 | }); 26 | } catch (error) { 27 | return { 28 | success: false, 29 | message: "Failed to delete license", 30 | }; 31 | } 32 | 33 | return { 34 | success: true, 35 | message: "License deleted successfully", 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/src/features/licensing/queries.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@rallly/database"; 2 | 3 | export async function getLicense() { 4 | return prisma.instanceLicense.findFirst(); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/features/navigation/command-menu/command-global-shortcut.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function cmdKey(e: KeyboardEvent) { 4 | if (e.metaKey || e.ctrlKey) { 5 | return e.key; 6 | } 7 | return false; 8 | } 9 | 10 | export function CommandGlobalShortcut({ trigger }: { trigger: () => void }) { 11 | React.useEffect(() => { 12 | const handleKeyDown = (e: KeyboardEvent) => { 13 | switch (cmdKey(e)) { 14 | case "k": 15 | e.preventDefault(); 16 | trigger(); 17 | break; 18 | } 19 | }; 20 | 21 | document.addEventListener("keydown", handleKeyDown); 22 | 23 | return () => { 24 | document.removeEventListener("keydown", handleKeyDown); 25 | }; 26 | }, [trigger]); 27 | 28 | // This component doesn't render anything 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/src/features/navigation/command-menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./command-menu"; 2 | -------------------------------------------------------------------------------- /apps/web/src/features/poll/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const pollStatusSchema = z.enum(["live", "paused", "finalized"]); 4 | 5 | export type PollStatus = z.infer; 6 | -------------------------------------------------------------------------------- /apps/web/src/features/quick-create/components/relative-date.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dayjs from "dayjs"; 3 | 4 | import { Trans } from "@/components/trans"; 5 | 6 | export function RelativeDate({ date }: { date: Date }) { 7 | return ( 8 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/features/quick-create/constants.ts: -------------------------------------------------------------------------------- 1 | export const isQuickCreateEnabled = process.env.QUICK_CREATE_ENABLED === "true"; 2 | -------------------------------------------------------------------------------- /apps/web/src/features/quick-create/index.ts: -------------------------------------------------------------------------------- 1 | export { isQuickCreateEnabled } from "./constants"; 2 | export { QuickCreateButton } from "./quick-create-button"; 3 | export { QuickCreateWidget } from "./quick-create-widget"; 4 | -------------------------------------------------------------------------------- /apps/web/src/features/quick-create/lib/get-guest-polls.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@rallly/database"; 2 | 3 | import { auth } from "@/next-auth"; 4 | 5 | export async function getGuestPolls() { 6 | const session = await auth(); 7 | const user = session?.user; 8 | const guestId = !user?.email ? user?.id : null; 9 | 10 | if (!guestId) { 11 | return []; 12 | } 13 | 14 | const recentlyCreatedPolls = await prisma.poll.findMany({ 15 | where: { 16 | guestId, 17 | deleted: false, 18 | }, 19 | select: { 20 | id: true, 21 | title: true, 22 | createdAt: true, 23 | }, 24 | orderBy: { 25 | createdAt: "desc", 26 | }, 27 | take: 3, 28 | }); 29 | 30 | return recentlyCreatedPolls; 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/src/features/quick-create/quick-create-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@rallly/ui/button"; 2 | import { Icon } from "@rallly/ui/icon"; 3 | import { ZapIcon } from "lucide-react"; 4 | import Link from "next/link"; 5 | import { Trans } from "react-i18next/TransWithoutContext"; 6 | 7 | import { getTranslation } from "@/i18n/server"; 8 | 9 | export async function QuickCreateButton() { 10 | const { t } = await getTranslation(); 11 | return ( 12 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/features/rate-limit/constants.ts: -------------------------------------------------------------------------------- 1 | export const isRateLimitEnabled = !!process.env.KV_REST_API_URL; 2 | -------------------------------------------------------------------------------- /apps/web/src/features/scheduled-event/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const statusSchema = z.enum([ 4 | "upcoming", 5 | "unconfirmed", 6 | "past", 7 | "canceled", 8 | ]); 9 | 10 | export type Status = z.infer; 11 | -------------------------------------------------------------------------------- /apps/web/src/features/setup/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const setupSchema = z.object({ 4 | name: z.string().min(1), 5 | timeZone: z.string().min(1), 6 | locale: z.string().min(1), 7 | }); 8 | 9 | export type SetupFormValues = z.infer; 10 | 11 | export const onboardedUserSchema = z.object({ 12 | name: z.string().min(1), 13 | timeZone: z.string().min(1), 14 | locale: z.string().min(1), 15 | }); 16 | 17 | export type OnboardedUser = z.infer; 18 | -------------------------------------------------------------------------------- /apps/web/src/features/setup/types.ts: -------------------------------------------------------------------------------- 1 | // Assuming you have a base User type somewhere, e.g., in prisma types or a shared types file 2 | import type { User as BaseUser } from "@prisma/client"; // Or wherever your base User type is 3 | 4 | // Define the type for a user who has completed onboarding 5 | // It extends the base user but makes required onboarding fields non-nullable 6 | export interface OnboardedUser extends BaseUser { 7 | name: string; // Guaranteed to be a string 8 | timeZone: string; // Guaranteed to be a string 9 | locale: string; // Guaranteed to be a string 10 | } 11 | 12 | // You might also want a type for the potentially un-onboarded user, 13 | // which could just be your BaseUser type 14 | export type PotentiallyUnonboardedUser = BaseUser; 15 | -------------------------------------------------------------------------------- /apps/web/src/features/storage/index.ts: -------------------------------------------------------------------------------- 1 | export const isStorageEnabled = 2 | !!process.env.S3_BUCKET_NAME && 3 | !!process.env.S3_ACCESS_KEY_ID && 4 | !!process.env.S3_SECRET_ACCESS_KEY; 5 | -------------------------------------------------------------------------------- /apps/web/src/features/storage/s3.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | 3 | import { env } from "@/env"; 4 | 5 | export function getS3Client() { 6 | if ( 7 | !env.S3_BUCKET_NAME || 8 | !env.S3_ACCESS_KEY_ID || 9 | !env.S3_SECRET_ACCESS_KEY 10 | ) { 11 | return null; 12 | } 13 | 14 | const s3Client = new S3Client({ 15 | region: env.S3_REGION, 16 | endpoint: env.S3_ENDPOINT, 17 | credentials: { 18 | accessKeyId: env.S3_ACCESS_KEY_ID, 19 | secretAccessKey: env.S3_SECRET_ACCESS_KEY, 20 | }, 21 | // S3 compatible storage requires path style 22 | forcePathStyle: true, 23 | }); 24 | 25 | return s3Client; 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/features/subscription/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const subscriptionCheckoutMetadataSchema = z.object({ 4 | userId: z.string(), 5 | }); 6 | 7 | export type SubscriptionCheckoutMetadata = z.infer< 8 | typeof subscriptionCheckoutMetadataSchema 9 | >; 10 | -------------------------------------------------------------------------------- /apps/web/src/features/timezone/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client/context"; 2 | export * from "./utils"; 3 | -------------------------------------------------------------------------------- /apps/web/src/features/user/schema.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | export const userRoleSchema = z.enum(["admin", "user"]); 4 | 5 | export type UserRole = z.infer; 6 | -------------------------------------------------------------------------------- /apps/web/src/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Sentry from "@sentry/nextjs"; 4 | import NextError from "next/error"; 5 | import React from "react"; 6 | 7 | export default function GlobalError({ 8 | error, 9 | }: { 10 | error: Error & { digest?: string }; 11 | }) { 12 | React.useEffect(() => { 13 | Sentry.captureException(error); 14 | }, [error]); 15 | 16 | return ( 17 | 18 | 19 | {/* `NextError` is the default Next.js error page component. Its type 20 | definition requires a `statusCode` prop. However, since the App Router 21 | does not expose status codes for errors, we simply pass 0 to render a 22 | generic error message. */} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/src/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { Namespace, i18n } from "i18next"; 2 | import { createInstance } from "i18next"; 3 | import ICU from "i18next-icu"; 4 | import { initReactI18next } from "react-i18next/initReactI18next"; 5 | 6 | import { getOptions } from "./settings"; 7 | 8 | export const initI18next = async ({ 9 | lng, 10 | ns, 11 | middleware, 12 | }: { 13 | lng: string; 14 | ns?: Namespace; 15 | middleware: (i18n: i18n) => void; 16 | }) => { 17 | const i18nInstance = createInstance().use(initReactI18next).use(ICU); 18 | middleware(i18nInstance); 19 | const t = await i18nInstance.init(getOptions(lng, ns)); 20 | return { t, i18n: i18nInstance }; 21 | }; 22 | -------------------------------------------------------------------------------- /apps/web/src/i18n/server.ts: -------------------------------------------------------------------------------- 1 | import resourcesToBackend from "i18next-resources-to-backend"; 2 | 3 | import { defaultNS } from "@/i18n/settings"; 4 | 5 | import { initI18next } from "./i18n"; 6 | import { getLocale } from "./server/get-locale"; 7 | 8 | export async function getTranslation(localeOverride?: string) { 9 | let locale = localeOverride; 10 | 11 | if (!locale) { 12 | locale = await getLocale(); 13 | } 14 | 15 | const { i18n } = await initI18next({ 16 | lng: locale, 17 | middleware: (i18n) => { 18 | i18n.use( 19 | resourcesToBackend( 20 | (language: string, namespace: string) => 21 | import(`../../public/locales/${language}/${namespace}.json`), 22 | ), 23 | ); 24 | }, 25 | }); 26 | return { 27 | t: i18n.getFixedT(locale, defaultNS), 28 | i18n, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/src/i18n/server/get-locale.ts: -------------------------------------------------------------------------------- 1 | import { defaultLocale } from "@rallly/languages"; 2 | import { headers } from "next/headers"; 3 | 4 | export async function getLocale() { 5 | const headersList = await headers(); 6 | const localeFromHeader = headersList.get("x-locale"); 7 | 8 | if (!localeFromHeader) { 9 | return defaultLocale; 10 | } 11 | return localeFromHeader; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/i18n/settings.ts: -------------------------------------------------------------------------------- 1 | import allLanguages from "@rallly/languages"; 2 | import type { InitOptions, Namespace } from "i18next"; 3 | 4 | export const fallbackLng = "en"; 5 | export const languages = Object.keys(allLanguages); 6 | export const defaultNS = "app"; 7 | 8 | export function getOptions( 9 | lng = fallbackLng, 10 | ns: Namespace = defaultNS, 11 | ): InitOptions { 12 | return { 13 | supportedLngs: languages, 14 | fallbackLng, 15 | lng, 16 | fallbackNS: defaultNS, 17 | defaultNS, 18 | ns, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/i18n/types.ts: -------------------------------------------------------------------------------- 1 | import type app from "../../public/locales/en/app.json"; 2 | 3 | export type TxKeyPath = keyof typeof app; 4 | -------------------------------------------------------------------------------- /apps/web/src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/nextjs"; 2 | 3 | export const onRequestError = Sentry.captureRequestError; 4 | 5 | export async function register() { 6 | if (process.env.NEXT_RUNTIME === "nodejs") { 7 | await import("../sentry.server.config"); 8 | } 9 | 10 | if (process.env.NEXT_RUNTIME === "edge") { 11 | await import("../sentry.edge.config"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/trpc/client.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from "@trpc/react-query"; 2 | 3 | import type { AppRouter } from "@/trpc/routers"; 4 | 5 | export const trpc = createTRPCReact({ 6 | overrides: { 7 | useMutation: { 8 | async onSuccess(opts) { 9 | await opts.originalFn(); 10 | await opts.queryClient.invalidateQueries(); 11 | }, 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /apps/web/src/trpc/client/types.ts: -------------------------------------------------------------------------------- 1 | import type { PollStatus, VoteType } from "@rallly/database"; 2 | 3 | export type GetPollApiResponse = { 4 | id: string; 5 | title: string; 6 | location: string | null; 7 | description: string | null; 8 | options: { id: string; startTime: Date; duration: number }[]; 9 | user: { 10 | id: string; 11 | name: string; 12 | email: string | null; 13 | image: string | null; 14 | banned: boolean; 15 | } | null; 16 | timeZone: string | null; 17 | adminUrlId: string; 18 | status: PollStatus; 19 | participantUrlId: string; 20 | createdAt: Date; 21 | deleted: boolean; 22 | }; 23 | 24 | export type Vote = { 25 | optionId: string; 26 | type: VoteType; 27 | }; 28 | -------------------------------------------------------------------------------- /apps/web/src/trpc/context.ts: -------------------------------------------------------------------------------- 1 | import type { EmailClient } from "@rallly/emails"; 2 | 3 | type User = { 4 | id: string; 5 | isGuest: boolean; 6 | locale?: string; 7 | getEmailClient: (locale?: string) => EmailClient; 8 | image?: string; 9 | }; 10 | 11 | export type TRPCContext = { 12 | user?: User; 13 | locale?: string; 14 | identifier?: string; 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/src/trpc/routers/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@rallly/database"; 2 | 3 | import { privateProcedure, router } from "../trpc"; 4 | 5 | export const dashboard = router({ 6 | info: privateProcedure.query(async ({ ctx }) => { 7 | const activePollCount = await prisma.poll.count({ 8 | where: { 9 | ...(ctx.user.isGuest 10 | ? { 11 | guestId: ctx.user.id, 12 | } 13 | : { 14 | userId: ctx.user.id, 15 | }), 16 | status: "live", 17 | deleted: false, // TODO (Luke Vella) [2024-06-16]: We should add deleted/cancelled to the status enum 18 | }, 19 | }); 20 | 21 | return { activePollCount }; 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /apps/web/src/trpc/routers/index.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import timezone from "dayjs/plugin/timezone"; 3 | import toArray from "dayjs/plugin/toArray"; 4 | import utc from "dayjs/plugin/utc"; 5 | 6 | import { mergeRouters, router } from "../trpc"; 7 | import { auth } from "./auth"; 8 | import { dashboard } from "./dashboard"; 9 | import { polls } from "./polls"; 10 | import { scheduledEvents } from "./scheduled-events"; 11 | import { user } from "./user"; 12 | 13 | dayjs.extend(toArray); // used for creating ics 14 | dayjs.extend(timezone); 15 | dayjs.extend(utc); 16 | 17 | export const appRouter = mergeRouters( 18 | router({ 19 | scheduledEvents, 20 | auth, 21 | polls, 22 | user, 23 | dashboard, 24 | }), 25 | ); 26 | 27 | export type AppRouter = typeof appRouter; 28 | -------------------------------------------------------------------------------- /apps/web/src/trpc/types.ts: -------------------------------------------------------------------------------- 1 | export type RegistrationTokenPayload = { 2 | name: string; 3 | email: string; 4 | locale?: string; 5 | timeZone?: string; 6 | code: string; 7 | }; 8 | 9 | export type DisableNotificationsPayload = { 10 | pollId: string; 11 | watcherId: number; 12 | }; 13 | 14 | export type RegisteredUserSession = { 15 | isGuest: false; 16 | id: string; 17 | name: string; 18 | email: string; 19 | }; 20 | 21 | export type GuestUserSession = { 22 | isGuest: true; 23 | id: string; 24 | }; 25 | 26 | export type UserSession = GuestUserSession | RegisteredUserSession; 27 | -------------------------------------------------------------------------------- /apps/web/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react"; 2 | import type React from "react"; 3 | 4 | export type ReactTag = keyof JSX.IntrinsicElements; 5 | 6 | export type PropsOf = TTag extends React.ElementType 7 | ? React.ComponentProps 8 | : never; 9 | 10 | export type PropsWithClassName> = 11 | React.PropsWithChildren & { 12 | className?: string; 13 | }; 14 | 15 | // biome-ignore lint/complexity/noBannedTypes: Fix this later 16 | export type IconComponent = {}> = 17 | React.ComponentType< 18 | React.PropsWithChildren & { 19 | className?: string; 20 | } 21 | >; 22 | -------------------------------------------------------------------------------- /apps/web/src/utils/api-auth.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import { NextResponse } from "next/server"; 3 | 4 | /** 5 | * Checks if the request is authorized using the API secret 6 | * @returns A NextResponse with 401 status if unauthorized, or null if authorized 7 | */ 8 | export async function checkApiAuthorization() { 9 | const headersList = await headers(); 10 | const authorization = headersList.get("authorization"); 11 | 12 | if (authorization !== `Bearer ${process.env.API_SECRET}`) { 13 | return NextResponse.json( 14 | { success: false, message: "Unauthorized" }, 15 | { status: 401 }, 16 | ); 17 | } 18 | 19 | return null; 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const isSelfHosted = process.env.NEXT_PUBLIC_SELF_HOSTED === "true"; 2 | 3 | export const isFeedbackEnabled = false; 4 | 5 | export const appVersion = process.env.NEXT_PUBLIC_APP_VERSION; 6 | -------------------------------------------------------------------------------- /apps/web/src/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | 3 | export function setCookie(key: string, value: string) { 4 | Cookies.set(key, value); 5 | } 6 | 7 | export function popCookie(key: string) { 8 | const value = Cookies.get(key); 9 | Cookies.remove(key); 10 | return value; 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/src/utils/get-value-by-path.ts: -------------------------------------------------------------------------------- 1 | export function getValueByPath>( 2 | obj: O, 3 | path: string, 4 | ): unknown { 5 | const pathArray = path.split("."); 6 | // biome-ignore lint/suspicious/noExplicitAny: Fix this later 7 | let curr: any = obj; 8 | for (const part of pathArray) { 9 | if (curr[part] === undefined) { 10 | return undefined; 11 | } 12 | curr = curr[part]; 13 | } 14 | return curr; 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/utils/grouped-time-zone.ts: -------------------------------------------------------------------------------- 1 | import { parseIanaTimezone } from "@/utils/date-time-utils"; 2 | import { supportedTimeZones } from "@/utils/supported-time-zones"; 3 | 4 | export const groupedTimeZones = supportedTimeZones.reduce( 5 | (acc, tz) => { 6 | const { region, city } = parseIanaTimezone(tz); 7 | if (!acc[region]) { 8 | acc[region] = []; 9 | } 10 | acc[region].push({ timezone: tz, city }); 11 | return acc; 12 | }, 13 | {} as Record, 14 | ); 15 | -------------------------------------------------------------------------------- /apps/web/src/utils/next.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiHandler } from "next"; 2 | 3 | export function composeApiHandlers(...fns: NextApiHandler[]): NextApiHandler { 4 | return async (req, res) => { 5 | for (const fn of fns) { 6 | await fn(req, res); 7 | if (res.writableEnded) { 8 | return; 9 | } 10 | } 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/utils/permissions.ts: -------------------------------------------------------------------------------- 1 | export function isOwner( 2 | resource: { userId?: string | null; guestId?: string | null }, 3 | user: { id: string; isGuest: boolean }, 4 | ) { 5 | if (user.isGuest) { 6 | return resource.guestId === user.id; 7 | } 8 | 9 | return resource.userId === user.id; 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/utils/posthog.ts: -------------------------------------------------------------------------------- 1 | import { posthog } from "@rallly/posthog/server"; 2 | import { waitUntil } from "@vercel/functions"; 3 | import type { NextRequest } from "next/server"; 4 | 5 | export function withPosthog(handler: (req: NextRequest) => Promise) { 6 | return async (req: NextRequest) => { 7 | const res = await handler(req); 8 | try { 9 | waitUntil(Promise.all([posthog?.shutdown()])); 10 | } catch (error) { 11 | console.error("Failed to flush PostHog events:", error); 12 | } 13 | return res; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/utils/selectors.ts: -------------------------------------------------------------------------------- 1 | export const getPortal = () => { 2 | const el = document.getElementById("portal"); 3 | if (el === null) { 4 | throw new Error("Portal element not found"); 5 | } 6 | return el; 7 | }; 8 | -------------------------------------------------------------------------------- /apps/web/src/utils/session/session-config.ts: -------------------------------------------------------------------------------- 1 | import { absoluteUrl } from "@rallly/utils/absolute-url"; 2 | 3 | export const sessionConfig = { 4 | password: process.env.SECRET_PASSWORD ?? "", 5 | cookieName: "rallly-session", 6 | cookieOptions: { 7 | secure: absoluteUrl().startsWith("https://") ?? false, 8 | }, 9 | ttl: 60 * 60 * 24 * 30, // 30 days 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/src/utils/subscription.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@rallly/database"; 2 | 3 | import { isSelfHosted } from "./constants"; 4 | 5 | export const getSubscriptionStatus = async (userId: string) => { 6 | const user = await prisma.user.findUnique({ 7 | where: { 8 | id: userId, 9 | }, 10 | select: { 11 | subscription: { 12 | select: { 13 | active: true, 14 | }, 15 | }, 16 | }, 17 | }); 18 | 19 | return { 20 | active: user?.subscription?.active === true || isSelfHosted, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require("@rallly/tailwind-config"); 2 | 3 | module.exports = sharedConfig; 4 | -------------------------------------------------------------------------------- /apps/web/tests/edit-options-page.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from "@playwright/test"; 2 | 3 | export class EditOptionsPage { 4 | constructor(public readonly page: Page) {} 5 | 6 | async switchToSpecifyTimes() { 7 | await this.page.click("[data-testid='specify-times-switch']"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/tests/i18n.spec.ts: -------------------------------------------------------------------------------- 1 | import test, { expect } from "@playwright/test"; 2 | 3 | test("should show correct language if supported", async ({ browser }) => { 4 | const context = await browser.newContext({ locale: "de" }); 5 | const page = await context.newPage(); 6 | await page.goto("/new"); 7 | await expect(page.locator("text=Titel")).toBeVisible(); 8 | }); 9 | 10 | test("should default to english", async ({ browser }) => { 11 | const context = await browser.newContext({ locale: "mt" }); 12 | const page = await context.newPage(); 13 | await page.goto("/new"); 14 | await expect(page.locator("text=Title")).toBeVisible(); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/web/tests/invite-page.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from "@playwright/test"; 2 | 3 | export class InvitePage { 4 | constructor(public readonly page: Page) {} 5 | 6 | async addParticipant(name: string, email?: string) { 7 | const page = this.page; 8 | 9 | await page.locator("data-testid=vote-selector >> nth=0").click(); 10 | await page.locator("data-testid=vote-selector >> nth=2").click(); 11 | await page.click("button >> text='Continue'"); 12 | 13 | await page.type('[placeholder="Jessie Smith"]', name); 14 | if (email) { 15 | await page.type('[placeholder="jessie.smith@example.com"]', email); 16 | } 17 | 18 | await page.click("text='Submit'"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/tests/test-utils.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from "@playwright/test"; 2 | import { prisma } from "@rallly/database"; 3 | import { LoginPage } from "./login-page"; 4 | 5 | export async function createUserInDb( 6 | email: string, 7 | name: string, 8 | role: "user" | "admin" = "user", 9 | ) { 10 | return prisma.user.create({ 11 | data: { 12 | email, 13 | name, 14 | role, 15 | locale: "en", 16 | timeZone: "Europe/London", 17 | emailVerified: new Date(), 18 | }, 19 | }); 20 | } 21 | 22 | export async function loginWithEmail(page: Page, email: string) { 23 | const loginPage = new LoginPage(page); 24 | await loginPage.goto(); 25 | await loginPage.login({ 26 | email, 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | 3 | import { captureEmailHTML } from "./mailpit/mailpit"; 4 | 5 | /** 6 | * Get the 6-digit code from the email 7 | * @param email The email address to get the code for 8 | * @returns 6-digit code 9 | */ 10 | export const getCode = async (email: string) => { 11 | const html = await captureEmailHTML(email); 12 | 13 | const $ = load(html); 14 | 15 | return $("#code").text().trim(); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rallly/tsconfig/next.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | }, 8 | "strictNullChecks": true, 9 | "types": ["vitest/globals"], 10 | "target": "ES2017" 11 | }, 12 | "include": [ 13 | "**/*.ts", 14 | "**/*.tsx", 15 | "**/*.js", 16 | ".next/types/**/*.ts", 17 | "vitest.config.mts" 18 | ], 19 | "exclude": ["node_modules", ".next/**/*", "playwright-report", "test-results"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "installCommand": "pnpm install", 3 | "buildCommand": "cd ../.. && pnpm db:generate && pnpm build:web && pnpm db:deploy", 4 | "outputDirectory": ".next" 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | include: ["**/*.test.ts"], 8 | exclude: ["**/node_modules/**", "**/*.spec.ts"], 9 | }, 10 | resolve: { 11 | alias: { 12 | "@": path.resolve(__dirname, "./src"), 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/assets/images/splash.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | project_id_env: CROWDIN_PROJECT_ID 2 | api_token_env: CROWDIN_PERSONAL_TOKEN 3 | pull_request_title: "🌐 New Crowdin updates" 4 | commit_message: "[ci skip]" 5 | 6 | files: 7 | - source: /apps/web/public/locales/en/*.json 8 | translation: /apps/web/public/locales/%two_letters_code%/%original_file_name% 9 | - source: /apps/landing/public/locales/en/*.json 10 | translation: /apps/landing/public/locales/%two_letters_code%/%original_file_name% 11 | - source: /packages/emails/locales/en/*.json 12 | translation: /packages/emails/locales/%two_letters_code%/%original_file_name% 13 | -------------------------------------------------------------------------------- /packages/billing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rallly/billing", 3 | "version": "0.0.0", 4 | "private": true, 5 | "exports": { 6 | ".": "./src/index.ts", 7 | "./*": "./src/*.ts" 8 | }, 9 | "scripts": { 10 | "checkout-expiry": "dotenv -e ../../.env -- tsx ./src/scripts/checkout-expiry.ts", 11 | "subscription-data-sync": "dotenv -e ../../.env -- tsx ./src/scripts/subscription-data-sync.ts", 12 | "sync-payment-methods": "dotenv -e ../../.env -- tsx ./src/scripts/sync-payment-methods.ts", 13 | "type-check": "tsc --pretty --noEmit" 14 | }, 15 | "dependencies": { 16 | "@radix-ui/react-radio-group": "^1.2.3", 17 | "@rallly/database": "workspace:*", 18 | "@rallly/ui": "workspace:*", 19 | "stripe": "^13.2.0" 20 | }, 21 | "devDependencies": { 22 | "@rallly/tsconfig": "workspace:*" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/billing/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/stripe"; 2 | -------------------------------------------------------------------------------- /packages/billing/src/lib/get-pricing.ts: -------------------------------------------------------------------------------- 1 | import { stripe } from ".."; 2 | 3 | export async function getPricing() { 4 | const prices = await stripe.prices.list({ 5 | lookup_keys: ["pro-monthly", "pro-yearly"], 6 | }); 7 | 8 | const [monthly, yearly] = prices.data; 9 | 10 | return { 11 | monthly: { 12 | currency: monthly.currency, 13 | price: monthly.unit_amount, 14 | }, 15 | yearly: { 16 | currency: yearly.currency, 17 | price: yearly.unit_amount, 18 | }, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/billing/src/pricing.ts: -------------------------------------------------------------------------------- 1 | export const pricingData = { 2 | monthly: { 3 | amount: 700, 4 | currency: "usd", 5 | }, 6 | yearly: { 7 | amount: 5600, 8 | currency: "usd", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/billing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rallly/tsconfig/next.json", 3 | "include": ["**/*.ts", "**/*.tsx", "**/*.js"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/database/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /packages/database/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export type * from "@prisma/client"; 4 | 5 | const prismaClientSingleton = () => { 6 | return new PrismaClient(); 7 | }; 8 | 9 | export type ExtendedPrismaClient = ReturnType; 10 | 11 | // biome-ignore lint/suspicious/noShadowRestrictedNames: Fix this later 12 | declare const globalThis: { 13 | prismaGlobal: ExtendedPrismaClient; 14 | } & typeof global; 15 | 16 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); 17 | 18 | export { prisma }; 19 | 20 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma; 21 | -------------------------------------------------------------------------------- /packages/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rallly/database", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "db:generate": "prisma generate", 7 | "db:push": "prisma db push --skip-generate", 8 | "db:deploy": "prisma migrate deploy", 9 | "db:migrate": "prisma migrate dev", 10 | "db:seed": "tsx prisma/seed.ts", 11 | "type-check": "tsc --pretty --noEmit" 12 | }, 13 | "exports": "./index.ts", 14 | "dependencies": { 15 | "@prisma/client": "^6.8.2", 16 | "dayjs": "^1.11.13" 17 | }, 18 | "devDependencies": { 19 | "@faker-js/faker": "^7.6.0", 20 | "@rallly/tsconfig": "workspace:*", 21 | "@types/node": "^20.11.1", 22 | "prisma": "^6.8.2", 23 | "tsx": "^4.6.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220322193753_add_comments/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Comment" ( 3 | "id" TEXT NOT NULL, 4 | "content" TEXT NOT NULL, 5 | "pollId" TEXT NOT NULL, 6 | "authorName" TEXT NOT NULL, 7 | "userId" TEXT, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | 10 | CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "Comment_id_pollId_key" ON "Comment"("id", "pollId"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "Comment" ADD CONSTRAINT "Comment_pollId_fkey" FOREIGN KEY ("pollId") REFERENCES "Poll"("urlId") ON DELETE RESTRICT ON UPDATE CASCADE; 21 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220330085423_add_notifications_column/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Poll" ADD COLUMN "notifications" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220408120721_legacy_column/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Poll" ADD COLUMN "closed" BOOLEAN NOT NULL DEFAULT false, 3 | ADD COLUMN "legacy" BOOLEAN NOT NULL DEFAULT false; 4 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220412112814_cascade_delete/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Comment" DROP CONSTRAINT "Comment_pollId_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "Link" DROP CONSTRAINT "Link_pollId_fkey"; 6 | 7 | -- DropForeignKey 8 | ALTER TABLE "Option" DROP CONSTRAINT "Option_pollId_fkey"; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "Link" ADD CONSTRAINT "Link_pollId_fkey" FOREIGN KEY ("pollId") REFERENCES "Poll"("urlId") ON DELETE CASCADE ON UPDATE CASCADE; 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "Option" ADD CONSTRAINT "Option_pollId_fkey" FOREIGN KEY ("pollId") REFERENCES "Poll"("urlId") ON DELETE CASCADE ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Comment" ADD CONSTRAINT "Comment_pollId_fkey" FOREIGN KEY ("pollId") REFERENCES "Poll"("urlId") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220412115407_cascade_delete_participants/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Participant" DROP CONSTRAINT "Participant_pollId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "Participant" ADD CONSTRAINT "Participant_pollId_fkey" FOREIGN KEY ("pollId") REFERENCES "Poll"("urlId") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220412115744_cascade_delete_votes/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Vote" DROP CONSTRAINT "Vote_pollId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_pollId_fkey" FOREIGN KEY ("pollId") REFERENCES "Poll"("urlId") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220506105524_sessions_update/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `verificationCode` on the `Poll` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "Poll_urlId_verificationCode_key"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Comment" ADD COLUMN "guestId" TEXT; 12 | 13 | -- AlterTable 14 | ALTER TABLE "Participant" ADD COLUMN "guestId" TEXT; 15 | 16 | -- AlterTable 17 | ALTER TABLE "Poll" DROP COLUMN "verificationCode"; 18 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220511113020_add_if_need_be/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "VoteType" AS ENUM ('yes', 'no', 'ifNeedBe'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "Vote" ADD COLUMN "type" "VoteType" NOT NULL DEFAULT E'yes'; 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220512093441_add_no_votes/migration.sql: -------------------------------------------------------------------------------- 1 | -- Previously we only stored "yes" and "ifNeedBe" votes and assumed missing votes are "no" 2 | -- Since we want to differentiate between "no" and did not vote yet we want to include "no" votes 3 | -- in this table 4 | INSERT INTO votes (id, poll_id, participant_id, option_id, type) 5 | SELECT nanoid(), poll_id, participant_id, option_id, 'no' 6 | FROM ( 7 | SELECT o.poll_id, p.id participant_id, o.id option_id 8 | FROM options o 9 | JOIN participants p ON o.poll_id = p.poll_id 10 | EXCEPT 11 | SELECT poll_id, participant_id, option_id 12 | FROM votes 13 | ) AS missing; 14 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220520115326_add_touch_column/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "polls" ADD COLUMN "touched_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220522165453_add_deleted_at_column/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "polls" ADD COLUMN "deleted_at" TIMESTAMP(3); 3 | 4 | UPDATE "polls" SET "deleted_at" = now() WHERE "deleted" = true; -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20220627191901_remove_user_relation/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `guest_id` on the `comments` table. All the data in the column will be lost. 5 | - You are about to drop the column `guest_id` on the `participants` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- Set user_id to guest_id 9 | UPDATE "comments" 10 | SET "user_id" = "guest_id" 11 | WHERE "user_id" IS NULL; 12 | 13 | -- AlterTable 14 | ALTER TABLE "comments" DROP COLUMN "guest_id"; 15 | 16 | -- Set user_id to guest_id 17 | UPDATE "participants" 18 | SET "user_id" = "guest_id" 19 | WHERE "user_id" IS NULL; 20 | 21 | -- AlterTable 22 | ALTER TABLE "participants" DROP COLUMN "guest_id"; 23 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20221026220835_add_indexes/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "participants_poll_id_idx" ON "participants" USING HASH ("poll_id"); 3 | 4 | -- CreateIndex 5 | CREATE INDEX "votes_participant_id_idx" ON "votes" USING HASH ("participant_id"); 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230117103853_update_indexes/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Participant_id_pollId_key"; 3 | 4 | -- DropIndex 5 | DROP INDEX "participants_id_poll_id_key"; 6 | 7 | -- CreateIndex 8 | CREATE INDEX "comments_poll_id_idx" ON "comments" USING HASH ("poll_id"); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "options_poll_id_idx" ON "options" USING HASH ("poll_id"); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "polls_user_id_idx" ON "polls" USING HASH ("user_id"); 15 | 16 | -- CreateIndex 17 | CREATE INDEX "votes_poll_id_idx" ON "votes" USING HASH ("poll_id"); 18 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230118192253_bring_back_comment_index/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "comments_user_id_idx" ON "comments" USING HASH ("user_id"); 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230118200546_add_unqiue_participant/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[id,poll_id]` on the table `participants` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "participants_id_poll_id_key" ON "participants"("id", "poll_id"); 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230303142641_add_participant_email/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "participants" ADD COLUMN "email" TEXT; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230318113511_remove_unecessary_unique_indexes/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Comment_id_pollId_key"; 3 | 4 | -- DropIndex 5 | DROP INDEX "comments_id_poll_id_key"; 6 | 7 | -- DropIndex 8 | DROP INDEX "participants_id_poll_id_key"; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230327105647_remove_author_name/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `author_name` on the `polls` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "polls" DROP COLUMN "author_name"; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230615111329_events/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `option_id` to the `events` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "events" ADD COLUMN "option_id" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230615163229_remove_selected_option_id/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `selected_option_id` on the `polls` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "polls" DROP COLUMN "selected_option_id"; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230704153346_user_payments/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "subscription_status" AS ENUM ('active', 'paused', 'deleted', 'trialing', 'past_due'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "user_payment_data" ( 6 | "user_id" TEXT NOT NULL, 7 | "subscription_id" TEXT NOT NULL, 8 | "plan_id" TEXT NOT NULL, 9 | "end_date" TIMESTAMP(3) NOT NULL, 10 | "status" "subscription_status" NOT NULL, 11 | "update_url" TEXT NOT NULL, 12 | "cancel_url" TEXT NOT NULL, 13 | 14 | CONSTRAINT "user_payment_data_pkey" PRIMARY KEY ("user_id") 15 | ); 16 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230721163042_hide_participants/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "polls" ADD COLUMN "hide_participants" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230725112615_add_poll_config/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "participant_visibility" AS ENUM ('full', 'scoresOnly', 'limited'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "polls" ADD COLUMN "disable_comments" BOOLEAN NOT NULL DEFAULT false, 6 | ADD COLUMN "hide_scores" BOOLEAN NOT NULL DEFAULT false; 7 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230915170216_add_required_email/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "polls" ADD COLUMN "require_participant_email" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231027074632_nextauth_ci_identifiers/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "verification_tokens" ALTER COLUMN "identifier" SET DATA TYPE CITEXT; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231117153753_add_nextauth_provider_accounts/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Account" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "type" TEXT NOT NULL, 6 | "provider" TEXT NOT NULL, 7 | "providerAccountId" TEXT NOT NULL, 8 | "refresh_token" TEXT, 9 | "access_token" TEXT, 10 | "expires_at" INTEGER, 11 | "token_type" TEXT, 12 | "scope" TEXT, 13 | "id_token" TEXT, 14 | "session_state" TEXT, 15 | 16 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 17 | ); 18 | 19 | -- CreateIndex 20 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 21 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231118134458_add_account_user_index/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "Account_userId_idx" ON "Account" USING HASH ("userId"); 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231205080854_fix_finalized_poll_status/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Fixes an issue in the previous migration where paused polls were not being 3 | set to finalized if they had an event_id. 4 | */ 5 | -- Set "status" to "finalized" if "event_id" is not null 6 | UPDATE "polls" 7 | SET "status" = 'finalized'::poll_status 8 | WHERE "event_id" IS NOT NULL; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240127064213_add_image_field/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "users" ADD COLUMN "image" TEXT; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240221084400_unset_invalid_timezones/migration.sql: -------------------------------------------------------------------------------- 1 | -- Unset non-geographic time zones 2 | UPDATE users SET time_zone = NULL WHERE time_zone NOT LIKE '%/%' OR time_zone LIKE 'Etc/%'; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240224011353_remove_legacy_user_preferences/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropTable 2 | DROP TABLE "user_preferences"; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240309043111_remove_poll_vote_index/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "votes_poll_id_idx"; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240309051319_add_option_vote_index/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "votes_option_id_idx" ON "votes" USING HASH ("option_id"); 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240315104329_index_votes_by_poll/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "votes_poll_id_idx" ON "votes" USING HASH ("poll_id"); 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240317095541_remove_legacy_start_column/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `start` on the `options` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "options" DROP COLUMN "start"; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240704085250_soft_delete_participants/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "participants" ADD COLUMN "deleted" BOOLEAN NOT NULL DEFAULT false, 3 | ADD COLUMN "deleted_at" TIMESTAMP(3); 4 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240901171230_participant_locale/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "participants" ADD COLUMN "locale" TEXT; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20250217082042_add_subscription_fields/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "subscriptions" ADD COLUMN "amount" INTEGER, 3 | ADD COLUMN "status" TEXT; 4 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20250221104854_add_cancel_at_period_end/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "subscriptions" ADD COLUMN "cancel_at_period_end" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20250222172325_add_payment_method_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "payment_methods" ( 3 | "id" TEXT NOT NULL, 4 | "user_id" TEXT NOT NULL, 5 | "type" TEXT NOT NULL, 6 | "data" JSONB NOT NULL, 7 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updated_at" TIMESTAMP(3) NOT NULL, 9 | 10 | CONSTRAINT "payment_methods_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "payment_methods" ADD CONSTRAINT "payment_methods_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20250226173250_remove_paddle_subscriptions/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropTable 2 | DROP TABLE "user_payment_data"; 3 | 4 | -- AlterEnum 5 | BEGIN; 6 | CREATE TYPE "subscription_status_new" AS ENUM ('incomplete', 'incomplete_expired', 'active', 'paused', 'trialing', 'past_due', 'canceled', 'unpaid'); 7 | ALTER TABLE "subscriptions" ALTER COLUMN "status" TYPE "subscription_status_new" USING ("status"::text::"subscription_status_new"); 8 | ALTER TYPE "subscription_status" RENAME TO "subscription_status_old"; 9 | ALTER TYPE "subscription_status_new" RENAME TO "subscription_status"; 10 | DROP TYPE "subscription_status_old"; 11 | COMMIT; -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20250302163530_add_user_ban_fields/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "users" ADD COLUMN "ban_reason" TEXT, 3 | ADD COLUMN "banned" BOOLEAN NOT NULL DEFAULT false, 4 | ADD COLUMN "banned_at" TIMESTAMP(3); 5 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20250402100733_remove_closed_column/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `closed` on the `polls` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "polls" DROP COLUMN "closed"; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20250522093338_add_user_role/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "user_role" AS ENUM ('admin', 'user'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "users" ADD COLUMN "role" "user_role" NOT NULL DEFAULT 'user'; 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20250523161200_remove_invitee_constraints/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "scheduled_event_invites_scheduled_event_id_invitee_email_key"; 3 | 4 | -- DropIndex 5 | DROP INDEX "scheduled_event_invites_scheduled_event_id_invitee_id_key"; 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20250526175615_remove_stripe_customer_id/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `stripe_customer_id` on the `licenses` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "licenses" DROP COLUMN "stripe_customer_id"; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /packages/database/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | directUrl = env("DIRECT_DATABASE_URL") 5 | } 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | binaryTargets = ["native"] 10 | previewFeatures = ["relationJoins"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/database/prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { seedPolls } from "./seed/polls"; 3 | import { seedScheduledEvents } from "./seed/scheduled-events"; 4 | import { seedUsers } from "./seed/users"; 5 | 6 | const prisma = new PrismaClient(); 7 | 8 | async function main() { 9 | const users = await seedUsers(); 10 | 11 | for (const user of users) { 12 | await seedPolls(user.id); 13 | await seedScheduledEvents(user.id); 14 | } 15 | } 16 | 17 | main() 18 | .catch((e) => console.error(e)) 19 | .finally(async () => { 20 | await prisma.$disconnect(); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/database/prisma/seed/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a random integer between floor and max (inclusive). 3 | * @param max The maximum value. 4 | * @param floor The minimum value (default: 0). 5 | * @returns A random integer. 6 | */ 7 | export const randInt = (max = 1, floor = 0): number => { 8 | return Math.round(Math.random() * max) + floor; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/database/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rallly/tsconfig/next.json", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/emails/.gitignore: -------------------------------------------------------------------------------- 1 | .react-email 2 | /out -------------------------------------------------------------------------------- /packages/emails/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import "i18next"; 2 | 3 | import type emails from "./locales/en/emails.json"; 4 | 5 | declare module "i18next" { 6 | interface CustomTypeOptions { 7 | defaultNS: "emails"; 8 | resources: { 9 | emails: typeof emails; 10 | }; 11 | returnNull: false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/emails/locales/th/emails.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/emails/locales/vi/emails.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/emails/src/components/email-context.tsx: -------------------------------------------------------------------------------- 1 | import { i18nDefaultConfig, i18nInstance } from "../i18n"; 2 | import type { EmailContext } from "../types"; 3 | 4 | i18nInstance.init({ 5 | ...i18nDefaultConfig, 6 | initImmediate: true, 7 | }); 8 | 9 | export const previewEmailContext: EmailContext = { 10 | logoUrl: "https://d39ixtfgglw55o.cloudfront.net/images/rallly-logo-mark.png", 11 | baseUrl: "https://rallly.co", 12 | domain: "rallly.co", 13 | supportEmail: "support@rallly.co", 14 | i18n: i18nInstance, 15 | t: i18nInstance.getFixedT("en"), 16 | }; 17 | -------------------------------------------------------------------------------- /packages/emails/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { SupportedEmailProviders } from "./send-email"; 2 | export { EmailClient } from "./send-email"; 3 | -------------------------------------------------------------------------------- /packages/emails/src/previews/abandoned-checkout.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { AbandonedCheckoutEmail } from "../templates/abandoned-checkout"; 3 | 4 | export default function AbandonedCheckoutEmailPreview() { 5 | return ( 6 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/emails/src/previews/change-email-request.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { ChangeEmailRequest } from "../templates/change-email-request"; 3 | 4 | export default function ChangeEmailRequestPreview() { 5 | return ( 6 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/emails/src/previews/finalized-host.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { FinalizeHostEmail } from "../templates/finalized-host"; 3 | 4 | export default function FinalizedHostPreview() { 5 | return ( 6 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/emails/src/previews/finalized-participant.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { FinalizeParticipantEmail } from "../templates/finalized-participant"; 3 | 4 | export default function FinalizedParticipantPreview() { 5 | return ( 6 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/emails/src/previews/license-key.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { LicenseKeyEmail } from "../templates/license-key"; 3 | 4 | export default function LicenseKeyPreview() { 5 | return ( 6 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/emails/src/previews/login.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { LoginEmail } from "../templates/login"; 3 | 4 | export default function LoginPreview() { 5 | return ( 6 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/emails/src/previews/new-comment.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { NewCommentEmail } from "../templates/new-comment"; 3 | 4 | function NewCommentEmailPreview() { 5 | return ( 6 | 13 | ); 14 | } 15 | 16 | export default NewCommentEmailPreview; 17 | -------------------------------------------------------------------------------- /packages/emails/src/previews/new-participant-confirmation.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { NewParticipantConfirmationEmail } from "../templates/new-participant-confirmation"; 3 | 4 | export default function NewParticipantConfirmationPreview() { 5 | return ( 6 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/emails/src/previews/new-participant.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { NewParticipantEmail } from "../templates/new-participant"; 3 | 4 | export default function NewParticipantPreview() { 5 | return ( 6 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/emails/src/previews/new-poll.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import NewPollEmail from "../templates/new-poll"; 3 | 4 | export default function NewPollPreview() { 5 | return ( 6 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/emails/src/previews/register.tsx: -------------------------------------------------------------------------------- 1 | import { previewEmailContext } from "../components/email-context"; 2 | import { RegisterEmail } from "../templates/register"; 3 | 4 | export default function RegisterEmailPreview() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /packages/emails/src/previews/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/packages/emails/src/previews/static/logo.png -------------------------------------------------------------------------------- /packages/emails/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { TFunction } from "i18next"; 2 | 3 | import type { I18nInstance } from "./i18n"; 4 | import type { EmailTemplates } from "./templates"; 5 | 6 | export type EmailContext = { 7 | logoUrl: string; 8 | baseUrl: string; 9 | domain: string; 10 | supportEmail: string; 11 | i18n: I18nInstance; 12 | t: TFunction; 13 | }; 14 | 15 | export type TemplateName = keyof EmailTemplates; 16 | 17 | export type TemplateProps = Omit< 18 | React.ComponentProps, 19 | "ctx" 20 | >; 21 | 22 | export type TemplateComponent = EmailTemplates[T] & { 23 | getSubject?: (props: TemplateProps, ctx: EmailContext) => string; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/emails/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rallly/tsconfig/next.json", 3 | "files": ["i18next.d.ts"], 4 | "include": ["**/*.ts", "**/*.tsx"], 5 | "exclude": ["node_modules", ".react-email"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/languages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rallly/languages", 3 | "version": "0.0.0", 4 | "private": true, 5 | "exports": { 6 | ".": "./src/index.ts", 7 | "./*": "./src/*.ts" 8 | }, 9 | "dependencies": { 10 | "@formatjs/intl-localematcher": "^0.6.0", 11 | "negotiator": "^1.0.0" 12 | }, 13 | "devDependencies": { 14 | "@rallly/tsconfig": "workspace:*", 15 | "@types/negotiator": "^0.6.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/languages/src/index.ts: -------------------------------------------------------------------------------- 1 | import { languages } from "./languages"; 2 | 3 | export type SupportedLocale = keyof typeof languages; 4 | 5 | export const supportedLngs = Object.keys(languages); 6 | 7 | export const defaultLocale = "en"; 8 | 9 | export default languages; 10 | 11 | export { languages }; 12 | -------------------------------------------------------------------------------- /packages/languages/src/languages.ts: -------------------------------------------------------------------------------- 1 | export const languages = { 2 | cs: "Česky", 3 | da: "Dansk", 4 | de: "Deutsch", 5 | en: "English", 6 | "en-GB": "English (UK)", 7 | es: "Español", 8 | fr: "Français", 9 | it: "Italiano", 10 | hu: "Magyar", 11 | nl: "Nederlands", 12 | no: "Norsk", 13 | pl: "Polski", 14 | pt: "Português", 15 | "pt-BR": "Português - Brasil", 16 | ru: "Русский", 17 | fi: "Suomi", 18 | sv: "Svenska", 19 | zh: "简体中文", 20 | }; 21 | -------------------------------------------------------------------------------- /packages/languages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rallly/tsconfig/node.json", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/posthog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rallly/posthog", 3 | "version": "0.0.0", 4 | "private": true, 5 | "exports": { 6 | "./server": "./src/server/index.ts", 7 | "./client": "./src/client.ts", 8 | "./utils": "./src/utils.ts" 9 | }, 10 | "scripts": { 11 | "type-check": "tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "js-cookie": "^3.0.1", 15 | "posthog-js": "^1.234.1", 16 | "posthog-node": "^4.10.2" 17 | }, 18 | "devDependencies": { 19 | "@rallly/tsconfig": "workspace:*", 20 | "@types/js-cookie": "^3.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/posthog/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const POSTHOG_BOOTSTAP_DATA_COOKIE_NAME = "posthog_bootstrap_data"; 2 | -------------------------------------------------------------------------------- /packages/posthog/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { PostHog } from "posthog-node"; 2 | 3 | function PostHogClient() { 4 | if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY) return null; 5 | 6 | const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, { 7 | host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST, 8 | flushAt: 1, 9 | flushInterval: 0, 10 | }); 11 | return posthogClient; 12 | } 13 | 14 | export const posthog = PostHogClient(); 15 | -------------------------------------------------------------------------------- /packages/posthog/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { POSTHOG_BOOTSTAP_DATA_COOKIE_NAME } from "./constants"; 2 | 3 | const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY; 4 | 5 | export function getPosthogBootstrapCookie(bootstrapData: { 6 | distinctID?: string; 7 | }) { 8 | if (!posthogApiKey) { 9 | return; 10 | } 11 | 12 | return { 13 | name: POSTHOG_BOOTSTAP_DATA_COOKIE_NAME, 14 | value: JSON.stringify(bootstrapData), 15 | httpOnly: false, 16 | secure: true, 17 | sameSite: "lax" as const, 18 | path: "/", 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/posthog/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rallly/tsconfig/next.json", 3 | "include": [ 4 | "**/*.ts", 5 | "**/*.tsx", 6 | "../../apps/web/src/posthog-page-view.tsx" 7 | ], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwind-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rallly/tailwind-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "tailwind.config.js", 6 | "types": "tailwind.config.d.ts", 7 | "dependencies": { 8 | "tailwindcss": "^3.4.17" 9 | }, 10 | "devDependencies": { 11 | "@tailwindcss/typography": "^0.5.13", 12 | "autoprefixer": "^10.4.13", 13 | "tailwind-scrollbar": "^3.0.4", 14 | "tailwindcss-animate": "^1.0.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/tailwind-config/tailwind.config.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@rallly/tailwind-config"; 2 | -------------------------------------------------------------------------------- /packages/tsconfig/next.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "@tsconfig/next/tsconfig.json", 5 | "compilerOptions": { 6 | "verbatimModuleSyntax": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/tsconfig/node.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node.js", 4 | "compilerOptions": { 5 | "lib": ["es2020"], 6 | "module": "NodeNext", 7 | "target": "es2020", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "NodeNext", 13 | "noUncheckedIndexedAccess": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rallly/tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "devDependencies": { 6 | "@tsconfig/next": "^2.0.1", 7 | "@tsconfig/strictest": "^2.0.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/tsconfig/react.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "noPropertyAccessFromIndexSignature": false, 8 | "exactOptionalPropertyTypes": false, 9 | "noUncheckedIndexedAccess": false, 10 | "noImplicitReturns": false, 11 | "verbatimModuleSyntax": true, 12 | "skipLibCheck": true, 13 | "strictNullChecks": true, 14 | "lib": ["dom", "dom.iterable", "esnext"], 15 | "jsx": "react-jsx" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@rallly/ui", 15 | "ui": "src", 16 | "utils": "@rallly/ui" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: React.FunctionComponent>; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /packages/ui/src/checkbox-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/ui/src/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 | 11 | export { Collapsible, CollapsibleContent, CollapsibleTrigger }; 12 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined, 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener("change", onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener("change", onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/use-platform.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export function usePlatform() { 4 | return { isMac: navigator.userAgent.includes("Mac") }; 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export { cn } from "./lib/utils"; 2 | -------------------------------------------------------------------------------- /packages/ui/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from "clsx"; 2 | import clsx from "clsx"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/src/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "./lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
    12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /packages/ui/src/styles/globals.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukevella/rallly/b19a498ffc19440774b7ed4676b6b470ae8d453a/packages/ui/src/styles/globals.css -------------------------------------------------------------------------------- /packages/ui/src/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useToast } from "./hooks/use-toast"; 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "./toast"; 11 | 12 | export function Toaster() { 13 | const { toasts } = useToast(); 14 | 15 | return ( 16 | 17 | {toasts.map(({ id, title, description, action, ...props }) => ( 18 | 19 |
    20 | {title && {title}} 21 | {description && {description}} 22 |
    23 | {action} 24 | 25 |
    26 | ))} 27 | 28 |
    29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@rallly/tailwind-config/tailwind.config"); 2 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rallly/tsconfig/next.json", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/prevent-widows"; 2 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rallly/utils", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test:unit": "vitest run", 7 | "type-check": "tsc --noEmit" 8 | }, 9 | "exports": { 10 | "./*": "./src/*.ts" 11 | }, 12 | "dependencies": { 13 | "nanoid": "^5.0.9" 14 | }, 15 | "devDependencies": { 16 | "@rallly/tsconfig": "workspace:*", 17 | "@types/node": "^20.11.1", 18 | "vitest": "^2.1.9" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/utils/src/nanoid.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from "nanoid"; 2 | 3 | export const nanoid = customAlphabet( 4 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 5 | 12, 6 | ); 7 | 8 | export const randomid = customAlphabet( 9 | "0123456789abcdefghijklmnopqrstuvwxyz", 10 | 12, 11 | ); 12 | 13 | export const generateOtp = customAlphabet("0123456789", 6); 14 | -------------------------------------------------------------------------------- /packages/utils/src/prevent-widows.ts: -------------------------------------------------------------------------------- 1 | export function preventWidows(text = "") { 2 | if (text.split(" ").length < 3) { 3 | return text; 4 | } 5 | const index = text.lastIndexOf(" "); 6 | return [text.substring(0, index), text.substring(index + 1)].join("\u00a0"); 7 | } 8 | -------------------------------------------------------------------------------- /packages/utils/src/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["vitest/globals", "node"], 4 | "lib": ["es2020", "dom"] 5 | }, 6 | "extends": "@rallly/tsconfig/node.json", 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/utils/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | include: ["**/*.test.ts"], 7 | exclude: ["**/node_modules/**", "**/*.spec.ts"], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - apps/* 3 | - packages/* 4 | 5 | onlyBuiltDependencies: 6 | - '@prisma/client' 7 | -------------------------------------------------------------------------------- /scripts/create-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | read -p "Enter the new version number: " new_version 4 | 5 | 6 | if [ ! -f "package.json" ]; then 7 | echo "Error: package.json file not found." 8 | exit 1 9 | fi 10 | 11 | # Replace the version in the package.json file 12 | sed -i "" "s/\"version\": \".*\"/\"version\": \"$new_version\"/g" package.json 13 | 14 | # Commit the changes with a message indicating the new version number 15 | git add package.json 16 | git commit -m "🔖 Release $new_version" 17 | 18 | # Tag the commit with the new version number (prefixed with "v") 19 | git tag -a "v$new_version" -m "Tag for version $new_version" 20 | 21 | echo "***v$new_version is ready for release 🚀***" 22 | -------------------------------------------------------------------------------- /scripts/docker-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | export DIRECT_DATABASE_URL=$DATABASE_URL 5 | export AUTH_URL=$NEXT_PUBLIC_BASE_URL 6 | 7 | pnpm prisma migrate deploy --schema=./prisma/schema.prisma 8 | node apps/web/server.js 9 | -------------------------------------------------------------------------------- /scripts/inject-version.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("node:child_process"); 2 | const packageJson = require("../package.json"); 3 | 4 | const version = packageJson.version; 5 | const gitHash = execSync("git rev-parse --short HEAD").toString().trim(); 6 | const versionWithHash = `${version}-${gitHash}`; 7 | 8 | console.log(versionWithHash); 9 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | export default ["packages/*/vitest.config.mts", "apps/*/vitest.config.mts"]; 2 | --------------------------------------------------------------------------------