mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-10 07:26:48 +02:00
♻️ Switch to app router (#922)
This commit is contained in:
parent
41f85279bb
commit
95feb9f01a
181 changed files with 2507 additions and 2494 deletions
11
.env.development
Normal file
11
.env.development
Normal file
|
@ -0,0 +1,11 @@
|
|||
# A random 32-character secret key used to encrypt user sessions
|
||||
SECRET_PASSWORD=abcdef1234567890abcdef1234567890
|
||||
# The base url where this instance is accessible, including the scheme.
|
||||
# Example: https://example.com
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
NEXTAUTH_URL=$NEXT_PUBLIC_BASE_URL
|
||||
# A connection string to your Postgres database
|
||||
DATABASE_URL="postgres://postgres:postgres@localhost:5450/db"
|
||||
|
||||
# Suppress warning from sentry during local development
|
||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=1
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"extends": ["next/core-web-vitals", "turbo"],
|
||||
"plugins": ["simple-import-sort", "@typescript-eslint"],
|
||||
"ignorePatterns": ["**/playwright-report/*.js"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["plugin:@typescript-eslint/recommended"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "error"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"import/first": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"no-console": ["error", { "allow": ["warn", "error", "info"] }],
|
||||
"no-unused-vars": "error"
|
||||
}
|
||||
}
|
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
|
@ -40,7 +40,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
steps:
|
||||
# Downloads a copy of the code in your repository before running CI tests
|
||||
- name: Check out repository code
|
||||
|
@ -56,21 +56,21 @@ jobs:
|
|||
node-version: 18
|
||||
cache: yarn
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "DATABASE_URL=postgresql://postgres:password@localhost:5432/rallly" >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "DATABASE_URL=postgresql://postgres:password@localhost:5450/db" >> $GITHUB_ENV
|
||||
|
||||
- name: Run db
|
||||
run: |
|
||||
docker pull postgres:14.2
|
||||
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_DB=rallly postgres:14.2
|
||||
yarn wait-on --timeout 60000 tcp:localhost:5432
|
||||
docker run -d -p 5450:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_DB=rallly postgres:14.2
|
||||
yarn wait-on --timeout 60000 tcp:localhost:5450
|
||||
|
||||
- name: Deploy migrations
|
||||
run: yarn db:deploy
|
||||
run: yarn db:setup
|
||||
|
||||
- name: Install playwright dependencies
|
||||
run: yarn playwright install --with-deps chromium
|
||||
|
|
62
README.md
62
README.md
|
@ -24,45 +24,53 @@ Built with [Next.js](https://github.com/vercel/next.js/), [Prisma](https://githu
|
|||
|
||||
Check out the [self-hosting docs](https://support.rallly.co/self-hosting) for more information on running your own instance of Rallly.
|
||||
|
||||
## Development
|
||||
## Get started
|
||||
|
||||
Clone this repo and change directory to the root of the repository.
|
||||
1. Clone the repository switch to the project directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lukevella/rallly.git
|
||||
cd rallly
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/lukevella/rallly.git
|
||||
cd rallly
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
2. Install dependencies
|
||||
|
||||
```
|
||||
yarn
|
||||
```
|
||||
```
|
||||
yarn
|
||||
```
|
||||
|
||||
Copy the sample `.env` file then open it and set the required [configuration options](https://support.rallly.co/self-hosting/configuration-options).
|
||||
3. Setup environment variables
|
||||
|
||||
```bash
|
||||
cp sample.env .env
|
||||
```
|
||||
```bash
|
||||
cp sample.env .env
|
||||
```
|
||||
|
||||
Next, run the following command:
|
||||
Create a `.env` file by copying `sample.env` then open it and set the required [configuration options](https://support.rallly.co/self-hosting/configuration-options).
|
||||
|
||||
```
|
||||
yarn db:generate && yarn db:reset
|
||||
```
|
||||
4. Setup the database
|
||||
|
||||
This will:
|
||||
If you don't have a postgres database running locally, you can spin up a new database using docker by running:
|
||||
|
||||
- generate the prisma database client
|
||||
- run migrations to create the database schema
|
||||
- seed the database with some random data
|
||||
```
|
||||
yarn dx
|
||||
```
|
||||
|
||||
Start the Next.js server
|
||||
If you already have a postgres database, you can run the migrations and seed the database by running:
|
||||
|
||||
```
|
||||
# For development
|
||||
yarn dev
|
||||
```
|
||||
```
|
||||
yarn db:setup
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- run migrations to create the database schema
|
||||
- seed the database with test users and random data
|
||||
|
||||
5. Start the Next.js server
|
||||
|
||||
```
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Contributors
|
||||
|
||||
|
|
4
apps/docs/.eslintrc.js
Normal file
4
apps/docs/.eslintrc.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
...require("@rallly/eslint-config")(__dirname),
|
||||
};
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"name": "@rallly/docs",
|
||||
"version": "0.0.0",
|
||||
"private": true
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@rallly/tsconfig": "*"
|
||||
}
|
||||
}
|
||||
|
|
5
apps/docs/tsconfig.json
Normal file
5
apps/docs/tsconfig.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@rallly/tsconfig/next.json",
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.js"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
4
apps/landing/.eslintrc.js
Normal file
4
apps/landing/.eslintrc.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
...require("@rallly/eslint-config")(__dirname),
|
||||
};
|
|
@ -20,7 +20,6 @@
|
|||
"@svgr/webpack": "^6.5.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@vercel/analytics": "^0.1.8",
|
||||
"@vercel/og": "^0.5.11",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"dayjs": "^1.11.7",
|
||||
|
@ -50,15 +49,8 @@
|
|||
"@types/smoothscroll-polyfill": "^0.3.1",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-turbo": "^0.0.9",
|
||||
"eslint-import-resolver-typescript": "^2.7.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"i18next-scanner": "^4.2.0",
|
||||
"i18next-scanner-typescript": "^1.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.8",
|
||||
"smtp-tester": "^2.0.1",
|
||||
"wait-on": "^6.0.1"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FrownIcon } from "@rallly/icons";
|
||||
import { FrownIcon } from "lucide-react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { cn } from "@rallly/ui";
|
||||
import { m } from "framer-motion";
|
||||
import {
|
||||
CalendarCheck2Icon,
|
||||
LanguagesIcon,
|
||||
Users2Icon,
|
||||
ZapIcon,
|
||||
} from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { m } from "framer-motion";
|
||||
} from "lucide-react";
|
||||
|
||||
import { Trans } from "@/components/trans";
|
||||
import { IconComponent } from "@/types";
|
||||
|
|
|
@ -40,7 +40,7 @@ function isBright(color: RGBColor): boolean {
|
|||
export const getRandomAvatarColor = (str: string) => {
|
||||
const strSum = str.split("").reduce((acc, val) => acc + val.charCodeAt(0), 0);
|
||||
const randomIndex = strSum % avatarBackgroundColors.length;
|
||||
const color = avatarBackgroundColors[randomIndex];
|
||||
const color = avatarBackgroundColors[randomIndex] as RGBColor;
|
||||
const [r, g, b] = color;
|
||||
return { color: `rgb(${r}, ${g}, ${b})`, requiresDarkText: isBright(color) };
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { ChevronRightIcon } from "@rallly/icons";
|
||||
import { Badge } from "@rallly/ui/badge";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { preventWidows } from "@rallly/utils";
|
||||
import { m } from "framer-motion";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NewspaperIcon } from "@rallly/icons";
|
||||
import { NewspaperIcon } from "lucide-react";
|
||||
import Script from "next/script";
|
||||
|
||||
import PageLayout from "@/components/layouts/page-layout";
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ChevronRightIcon, MenuIcon } from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
@ -7,6 +6,7 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@rallly/ui/dropdown-menu";
|
||||
import { ChevronRightIcon, MenuIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
LanguagesIcon,
|
||||
LinkedinIcon,
|
||||
TwitterIcon,
|
||||
} from "@rallly/icons";
|
||||
import { DiscordIcon } from "@rallly/icons";
|
||||
import languages from "@rallly/languages";
|
||||
import {
|
||||
Select,
|
||||
|
@ -13,6 +7,12 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@rallly/ui/select";
|
||||
import {
|
||||
GithubIcon,
|
||||
LanguagesIcon,
|
||||
LinkedinIcon,
|
||||
TwitterIcon,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ArrowUpRight } from "@rallly/icons";
|
||||
import { m } from "framer-motion";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { NextSeo } from "next-seo";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FileSearchIcon } from "@rallly/icons";
|
||||
import { FileSearchIcon } from "lucide-react";
|
||||
import { GetStaticProps } from "next";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ImageResponse } from "@vercel/og";
|
||||
import { ImageResponse } from "next/og";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const config = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ArrowLeftIcon } from "@rallly/icons";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import ErrorPage from "next/error";
|
||||
import Head from "next/head";
|
||||
|
@ -117,6 +117,8 @@ export async function getStaticProps(ctx: GetStaticPropsContext) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { TrendingUpIcon } from "@rallly/icons";
|
||||
import {
|
||||
BillingPlan,
|
||||
BillingPlanDescription,
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
} from "@rallly/ui/billing-plan";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
||||
import { TrendingUpIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { NextSeo } from "next-seo";
|
||||
|
|
|
@ -1,24 +1,13 @@
|
|||
{
|
||||
"extends": "@rallly/tsconfig/next.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"~/*": ["public/*"]
|
||||
},
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
"checkJs": false
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next-i18next.config.js"],
|
||||
"exclude": ["node_modules", "**/*.js"]
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@ PORT=3002
|
|||
NEXT_PUBLIC_BASE_URL=http://localhost:3002
|
||||
NEXTAUTH_URL=http://localhost:3002
|
||||
SECRET_PASSWORD=abcdefghijklmnopqrstuvwxyz1234567890
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/rallly
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5450/db
|
||||
SUPPORT_EMAIL=support@rallly.co
|
4
apps/web/.eslintrc.js
Normal file
4
apps/web/.eslintrc.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
...require("@rallly/eslint-config")(__dirname),
|
||||
};
|
2
apps/web/declarations/i18next.d.ts
vendored
2
apps/web/declarations/i18next.d.ts
vendored
|
@ -1,4 +1,4 @@
|
|||
import "react-i18next";
|
||||
import "i18next";
|
||||
|
||||
import app from "../public/locales/en/app.json";
|
||||
|
||||
|
|
1
apps/web/next-env.d.ts
vendored
1
apps/web/next-env.d.ts
vendored
|
@ -1,5 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
|
|
@ -4,17 +4,16 @@
|
|||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
const i18n = require("./i18n.config.js");
|
||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
i18n: { ...i18n, localeDetection: false },
|
||||
productionBrowserSourceMaps: true,
|
||||
output: "standalone",
|
||||
transpilePackages: [
|
||||
"@rallly/backend",
|
||||
"@rallly/database",
|
||||
"@rallly/icons",
|
||||
"@rallly/ui",
|
||||
"@rallly/tailwind-config",
|
||||
|
@ -22,12 +21,14 @@ const nextConfig = {
|
|||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
issuer: /\.[jt]sx?$/,
|
||||
use: ["@svgr/webpack"],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
@ -61,6 +62,7 @@ const sentryWebpackPluginOptions = {
|
|||
// recommended:
|
||||
// release, url, org, project, authToken, configFile, stripPrefix,
|
||||
// urlPrefix, include, ignore
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
dryRun: !process.env.SENTRY_AUTH_TOKEN,
|
||||
silent: true, // Suppresses all logs
|
||||
// For all available options, see:
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env TAILWIND_MODE=watch next dev",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build:test": "dotenv -e .env.test -- next build",
|
||||
"analyze": "cross-env ANALYZE=true next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
|
@ -29,7 +30,6 @@
|
|||
"@rallly/languages": "*",
|
||||
"@rallly/tailwind-config": "*",
|
||||
"@rallly/ui": "*",
|
||||
"@sentry/nextjs": "^7.74.1",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tanstack/react-query": "^4.0.0",
|
||||
|
@ -37,7 +37,6 @@
|
|||
"@trpc/client": "^10.13.0",
|
||||
"@trpc/next": "^10.13.0",
|
||||
"@trpc/react-query": "^10.13.0",
|
||||
"@vercel/og": "^0.5.13",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
|
@ -48,6 +47,7 @@
|
|||
"dayjs": "^1.11.10",
|
||||
"i18next": "^22.4.9",
|
||||
"i18next-icu": "^2.3.0",
|
||||
"i18next-resources-to-backend": "^1.1.4",
|
||||
"ics": "^3.1.0",
|
||||
"intl-messageformat": "^10.3.4",
|
||||
"iron-session": "^6.3.1",
|
||||
|
@ -72,11 +72,12 @@
|
|||
"smoothscroll-polyfill": "^0.4.4",
|
||||
"spacetime": "^7.4.7",
|
||||
"superjson": "^2.0.0",
|
||||
"timezone-soft": "^1.4.1"
|
||||
"timezone-soft": "^1.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@rallly/tsconfig": "*",
|
||||
"@rallly/eslint-config": "*",
|
||||
"@types/accept-language-parser": "^1.5.3",
|
||||
"@types/color-hash": "^1.0.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
|
@ -87,15 +88,8 @@
|
|||
"@types/smoothscroll-polyfill": "^0.3.1",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-turbo": "^0.0.9",
|
||||
"eslint-import-resolver-typescript": "^2.7.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"i18next-scanner": "^4.2.0",
|
||||
"i18next-scanner-typescript": "^1.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.8",
|
||||
"smtp-tester": "^2.0.1",
|
||||
"wait-on": "^6.0.1"
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import path from "path";
|
|||
|
||||
const ci = process.env.CI === "true";
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, "../../", ".env.test") });
|
||||
dotenv.config({ path: path.resolve(__dirname, ".env.test") });
|
||||
|
||||
// Use process.env.PORT by default and fallback to port 3000
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
@ -24,13 +24,9 @@ const config: PlaywrightTestConfig = {
|
|||
trace: "retain-on-failure",
|
||||
},
|
||||
webServer: {
|
||||
command: `NODE_ENV=test yarn dev --port ${PORT}`,
|
||||
command: `NODE_ENV=test yarn start --port ${PORT}`,
|
||||
url: baseURL,
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !ci,
|
||||
},
|
||||
expect: {
|
||||
timeout: 10000, // 10 seconds
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
reporter: [
|
||||
[ci ? "github" : "list"],
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: ["tailwindcss", "autoprefixer"],
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -225,5 +225,6 @@
|
|||
"authErrorCta": "Go to login page",
|
||||
"continueAs": "Continue as",
|
||||
"finalizeFeature": "Finalize",
|
||||
"duplicateFeature": "Duplicate"
|
||||
"duplicateFeature": "Duplicate",
|
||||
"pageMovedDescription": "Redirecting to <a>{newUrl}</a>"
|
||||
}
|
||||
|
|
6
apps/web/src/app/[locale]/(admin)/layout.tsx
Normal file
6
apps/web/src/app/[locale]/(admin)/layout.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
"use client";
|
||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <StandardLayout>{children}</StandardLayout>;
|
||||
}
|
17
apps/web/src/app/[locale]/(admin)/new/page.tsx
Normal file
17
apps/web/src/app/[locale]/(admin)/new/page.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { getTranslation } from "@/app/i18n";
|
||||
import { CreatePoll } from "@/components/create-poll";
|
||||
|
||||
export default function Page() {
|
||||
return <CreatePoll />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("newPoll"),
|
||||
};
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
"use client";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
|
@ -17,18 +18,16 @@ import {
|
|||
} from "@rallly/ui/form";
|
||||
import { Input } from "@rallly/ui/input";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
||||
import { PayWall } from "@/components/pay-wall";
|
||||
import { usePoll } from "@/components/poll-context";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string().trim().min(1),
|
||||
|
@ -63,7 +62,7 @@ const Page: NextPageWithLayout = () => {
|
|||
pollId: poll.id,
|
||||
newPollId: res.id,
|
||||
});
|
||||
await router.push(`/poll/${res.id}`);
|
||||
router.push(`/poll/${res.id}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -124,15 +123,4 @@ const Page: NextPageWithLayout = () => {
|
|||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getPollLayout;
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
export default Page;
|
|
@ -1,3 +1,4 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
@ -9,19 +10,17 @@ import {
|
|||
} from "@rallly/ui/card";
|
||||
import { Form } from "@rallly/ui/form";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import {
|
||||
PollDetailsData,
|
||||
PollDetailsForm,
|
||||
} from "@/components/forms/poll-details-form";
|
||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
||||
import { useUpdatePollMutation } from "@/components/poll/mutations";
|
||||
import { usePoll } from "@/components/poll-context";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { poll } = usePoll();
|
||||
|
@ -87,15 +86,4 @@ const Page: NextPageWithLayout = () => {
|
|||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getPollLayout;
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
export default Page;
|
|
@ -1,22 +1,20 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { CardFooter } from "@rallly/ui/card";
|
||||
import { Form } from "@rallly/ui/form";
|
||||
import dayjs from "dayjs";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { PollOptionsData } from "@/components/forms";
|
||||
import PollOptionsForm from "@/components/forms/poll-options-form";
|
||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
||||
import { useModalContext } from "@/components/modal/modal-provider";
|
||||
import { useUpdatePollMutation } from "@/components/poll/mutations";
|
||||
import { usePoll } from "@/components/poll-context";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { encodeDateOption } from "@/utils/date-time-utils";
|
||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
const convertOptionToString = (option: { start: Date; duration: number }) => {
|
||||
const start = dayjs(option.start).utc();
|
||||
|
@ -27,7 +25,7 @@ const convertOptionToString = (option: { start: Date; duration: number }) => {
|
|||
.format("YYYY-MM-DDTHH:mm:ss")}`;
|
||||
};
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const Page = () => {
|
||||
const { poll, getParticipantsWhoVotedForOption } = usePoll();
|
||||
const { mutate: updatePollMutation, isLoading: isUpdating } =
|
||||
useUpdatePollMutation();
|
||||
|
@ -136,15 +134,4 @@ const Page: NextPageWithLayout = () => {
|
|||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getPollLayout;
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
return {
|
||||
paths: [], //indicates that no page needs be created at build time
|
||||
fallback: "blocking", //indicates the type of fallback
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
export default Page;
|
|
@ -1,22 +1,20 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { CardFooter } from "@rallly/ui/card";
|
||||
import { Form } from "@rallly/ui/form";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import {
|
||||
PollSettingsForm,
|
||||
PollSettingsFormData,
|
||||
} from "@/components/forms/poll-settings";
|
||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
||||
import { useUpdatePollMutation } from "@/components/poll/mutations";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { usePoll } from "@/contexts/poll";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const Page = () => {
|
||||
const poll = usePoll();
|
||||
|
||||
const router = useRouter();
|
||||
|
@ -69,15 +67,4 @@ const Page: NextPageWithLayout = () => {
|
|||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getPollLayout;
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
export default Page;
|
|
@ -1,3 +1,4 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
CardContent,
|
||||
|
@ -6,10 +7,9 @@ import {
|
|||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@rallly/ui/card";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Card } from "@/components/card";
|
||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
||||
import { PayWall } from "@/components/pay-wall";
|
||||
import { FinalizePollForm } from "@/components/poll/manage-poll/finalize-poll-dialog";
|
||||
import { Trans } from "@/components/trans";
|
||||
|
@ -18,7 +18,6 @@ import { usePoll } from "@/contexts/poll";
|
|||
import { NextPageWithLayout } from "@/types";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
const FinalizationForm = () => {
|
||||
const plan = usePlan();
|
||||
|
@ -94,15 +93,4 @@ const Page: NextPageWithLayout = () => {
|
|||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getPollLayout;
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
export default Page;
|
31
apps/web/src/app/[locale]/(admin)/poll/[urlId]/layout.tsx
Normal file
31
apps/web/src/app/[locale]/(admin)/poll/[urlId]/layout.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { PollLayout } from "@/components/layouts/poll-layout";
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: React.PropsWithChildren<{ params: { urlId: string } }>) {
|
||||
const poll = await prisma.poll.findUnique({ where: { id: params.urlId } });
|
||||
if (!poll) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <PollLayout>{children}</PollLayout>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string; urlId: string };
|
||||
}) {
|
||||
const poll = await prisma.poll.findUnique({ where: { id: params.urlId } });
|
||||
|
||||
if (!poll) {
|
||||
return notFound();
|
||||
}
|
||||
return {
|
||||
title: poll.title,
|
||||
};
|
||||
}
|
|
@ -1,17 +1,14 @@
|
|||
import { InfoIcon } from "@rallly/icons";
|
||||
"use client";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
||||
import { LoginLink } from "@/components/login-link";
|
||||
import { Poll } from "@/components/poll";
|
||||
import { RegisterLink } from "@/components/register-link";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
import { usePoll } from "@/contexts/poll";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { isSelfHosted } from "@/utils/constants";
|
||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
const GuestPollAlert = () => {
|
||||
const poll = usePoll();
|
||||
|
@ -49,25 +46,11 @@ const GuestPollAlert = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className={cn("mx-auto w-full max-w-4xl space-y-3 sm:space-y-4")}>
|
||||
<GuestPollAlert />
|
||||
<Poll />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getPollLayout;
|
||||
Page.isAuthRequired = isSelfHosted;
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
export default Page;
|
||||
}
|
18
apps/web/src/app/[locale]/(admin)/polls/page.tsx
Normal file
18
apps/web/src/app/[locale]/(admin)/polls/page.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
import { PollsPage } from "./polls-page";
|
||||
|
||||
export default function Page() {
|
||||
return <PollsPage />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("polls"),
|
||||
};
|
||||
}
|
|
@ -1,19 +1,17 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
InboxIcon,
|
||||
PauseCircleIcon,
|
||||
PlusIcon,
|
||||
RadioIcon,
|
||||
VoteIcon,
|
||||
} from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import dayjs from "dayjs";
|
||||
import Head from "next/head";
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
import { DateIcon } from "@/components/date-icon";
|
||||
import { getStandardLayout } from "@/components/layouts/standard-layout";
|
||||
import {
|
||||
TopBar,
|
||||
TopBarTitle,
|
||||
|
@ -22,11 +20,8 @@ import { ParticipantAvatarBar } from "@/components/participant-avatar-bar";
|
|||
import { PollStatusBadge } from "@/components/poll-status";
|
||||
import { Skeleton } from "@/components/skeleton";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { isSelfHosted } from "@/utils/constants";
|
||||
import { useDayjs } from "@/utils/dayjs";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
const EmptyState = () => {
|
||||
return (
|
||||
|
@ -57,16 +52,12 @@ const EmptyState = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
export function PollsPage() {
|
||||
const { data } = trpc.polls.list.useQuery();
|
||||
const { t } = useTranslation();
|
||||
const { adjustTimeZone } = useDayjs();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{t("polls")}</title>
|
||||
</Head>
|
||||
<TopBar className="flex items-center justify-between gap-4">
|
||||
<TopBarTitle title={<Trans i18nKey="polls" />} icon={VoteIcon} />
|
||||
<div>
|
||||
|
@ -185,11 +176,4 @@ const Page: NextPageWithLayout = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getStandardLayout;
|
||||
Page.isAuthRequired = isSelfHosted;
|
||||
|
||||
export default Page;
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
}
|
|
@ -1,16 +1,15 @@
|
|||
import { ArrowUpRight, CreditCardIcon, SendIcon } from "@rallly/icons";
|
||||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Card } from "@rallly/ui/card";
|
||||
import { Label } from "@rallly/ui/label";
|
||||
import dayjs from "dayjs";
|
||||
import { GetStaticProps } from "next";
|
||||
import { ArrowUpRight, CreditCardIcon, SendIcon } from "lucide-react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import Script from "next/script";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import { BillingPlans } from "@/components/billing/billing-plans";
|
||||
import { getProfileLayout } from "@/components/layouts/profile-layout";
|
||||
import {
|
||||
Settings,
|
||||
SettingsContent,
|
||||
|
@ -19,12 +18,8 @@ import {
|
|||
} from "@/components/settings/settings";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { useSubscription } from "@/contexts/plan";
|
||||
import { isSelfHosted } from "@/utils/constants";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
import { NextPageWithLayout } from "../../types";
|
||||
import { getStaticTranslations } from "../../utils/with-page-translations";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -61,11 +56,7 @@ const BillingPortal = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const proPlanIdMonthly = process.env
|
||||
.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string;
|
||||
|
||||
export const proPlanIdYearly = process.env
|
||||
.NEXT_PUBLIC_PRO_PLAN_ID_YEARLY as string;
|
||||
const proPlanIdMonthly = process.env.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string;
|
||||
|
||||
const SubscriptionStatus = () => {
|
||||
const data = useSubscription();
|
||||
|
@ -106,7 +97,7 @@ const LegacyBilling = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_PADDLE_VENDOR_ID ? (
|
||||
<Script
|
||||
src="https://cdn.paddle.com/paddle/paddle.js"
|
||||
|
@ -215,7 +206,104 @@ const LegacyBilling = () => {
|
|||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<Card>
|
||||
<div className="grid gap-4 p-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label>
|
||||
<Trans i18nKey="billingStatusState" defaults="Status" />
|
||||
</Label>
|
||||
<div>
|
||||
{(() => {
|
||||
switch (status) {
|
||||
case "active":
|
||||
return (
|
||||
<Trans i18nKey="billingStatusActive" defaults="Active" />
|
||||
);
|
||||
case "paused":
|
||||
return (
|
||||
<Trans i18nKey="billingStatusPaused" defaults="Paused" />
|
||||
);
|
||||
case "deleted":
|
||||
return (
|
||||
<Trans
|
||||
i18nKey="billingStatusDeleted"
|
||||
defaults="Cancelled"
|
||||
/>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{status === "deleted" ? (
|
||||
<Label>
|
||||
<Trans i18nKey="endDate" defaults="End date" />
|
||||
</Label>
|
||||
) : (
|
||||
<Label>
|
||||
<Trans i18nKey="dueDate" defaults="Due date" />
|
||||
</Label>
|
||||
)}
|
||||
<div>{dayjs(endDate).format("LL")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
<Trans i18nKey="billingStatusPlan" defaults="Plan" />
|
||||
</Label>
|
||||
<div>
|
||||
<Trans i18nKey="planPro" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
<Trans i18nKey="billingPeriod" defaults="Period" />
|
||||
</Label>
|
||||
<div>
|
||||
{planId === proPlanIdMonthly ? (
|
||||
<Trans i18nKey="billingPeriodMonthly" defaults="Monthly" />
|
||||
) : (
|
||||
<Trans i18nKey="billingPeriodYearly" defaults="Yearly" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{status === "active" || status === "paused" ? (
|
||||
<div className="flex items-center gap-x-2 border-t bg-gray-50 p-3">
|
||||
<Button
|
||||
asChild
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.Paddle.Checkout.open({
|
||||
override: userPaymentData.updateUrl,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Link href={userPaymentData.updateUrl}>
|
||||
<CreditCardIcon className="h-4 w-4" />
|
||||
<Trans
|
||||
i18nKey="subscriptionUpdatePayment"
|
||||
defaults="Update Payment Details"
|
||||
/>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="destructive"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.Paddle.Checkout.open({
|
||||
override: userPaymentData.cancelUrl,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Link href={userPaymentData.cancelUrl}>
|
||||
<Trans i18nKey="subscriptionCancel" defaults="Cancel" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -241,7 +329,7 @@ const LegacyBilling = () => {
|
|||
</div>
|
||||
</SettingsSection>;
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
export function BillingPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
|
@ -291,18 +379,4 @@ const Page: NextPageWithLayout = () => {
|
|||
</SettingsContent>
|
||||
</Settings>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getProfileLayout;
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||
// This page is only available on the hosted version
|
||||
if (isSelfHosted) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
return await getStaticTranslations(ctx);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
}
|
14
apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx
Normal file
14
apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { BillingPage } from "@/app/[locale]/(admin)/settings/billing/billing-page";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
export default function Page() {
|
||||
return <BillingPage />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Params }) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("billing"),
|
||||
};
|
||||
}
|
10
apps/web/src/app/[locale]/(admin)/settings/layout.tsx
Normal file
10
apps/web/src/app/[locale]/(admin)/settings/layout.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
"use client";
|
||||
import { ProfileLayout } from "@/components/layouts/profile-layout";
|
||||
|
||||
export default function SettingsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <ProfileLayout>{children}</ProfileLayout>;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { PreferencesPage } from "@/app/[locale]/(admin)/settings/preferences/preferences-page";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
export default function Page() {
|
||||
return <PreferencesPage />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Params }) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("preferences"),
|
||||
};
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
import Head from "next/head";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import { getProfileLayout } from "@/components/layouts/profile-layout";
|
||||
import { useTranslation } from "@/app/i18n/client";
|
||||
import { DateTimePreferences } from "@/components/settings/date-time-preferences";
|
||||
import { LanguagePreference } from "@/components/settings/language-preference";
|
||||
import {
|
||||
|
@ -12,10 +12,7 @@ import {
|
|||
} from "@/components/settings/settings";
|
||||
import { Trans } from "@/components/trans";
|
||||
|
||||
import { NextPageWithLayout } from "../../types";
|
||||
import { getStaticTranslations } from "../../utils/with-page-translations";
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
export function PreferencesPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
|
@ -52,10 +49,4 @@ const Page: NextPageWithLayout = () => {
|
|||
</SettingsContent>
|
||||
</Settings>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getProfileLayout;
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
export default Page;
|
||||
}
|
14
apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx
Normal file
14
apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { ProfilePage } from "@/app/[locale]/(admin)/settings/profile/profile-page";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
export default function Page() {
|
||||
return <ProfilePage />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Params }) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("profile"),
|
||||
};
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
import Head from "next/head";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import { getProfileLayout } from "@/components/layouts/profile-layout";
|
||||
import { ProfileSettings } from "@/components/settings/profile-settings";
|
||||
import {
|
||||
Settings,
|
||||
|
@ -11,10 +11,7 @@ import {
|
|||
import { Trans } from "@/components/trans";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
|
||||
import { NextPageWithLayout } from "../../types";
|
||||
import { getStaticTranslations } from "../../utils/with-page-translations";
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
export const ProfilePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useUser();
|
||||
|
||||
|
@ -69,10 +66,3 @@ const Page: NextPageWithLayout = () => {
|
|||
</Settings>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = getProfileLayout;
|
||||
Page.isAuthRequired = true;
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
export default Page;
|
7
apps/web/src/app/[locale]/(auth)/layout.tsx
Normal file
7
apps/web/src/app/[locale]/(auth)/layout.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { AuthLayout } from "@/components/auth/auth-layout";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <AuthLayout>{children}</AuthLayout>;
|
||||
}
|
14
apps/web/src/app/[locale]/(auth)/login/page.tsx
Normal file
14
apps/web/src/app/[locale]/(auth)/login/page.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Params } from "@/app/[locale]/types";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
import { LoginForm } from "@/components/auth/auth-forms";
|
||||
|
||||
export default function LoginPage() {
|
||||
return <LoginForm />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Params }) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("login"),
|
||||
};
|
||||
}
|
14
apps/web/src/app/[locale]/(auth)/register/page.tsx
Normal file
14
apps/web/src/app/[locale]/(auth)/register/page.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { RegisterForm } from "@/app/[locale]/(auth)/register/register-page";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
export default function Page() {
|
||||
return <RegisterForm />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Params }) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("register"),
|
||||
};
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
@ -9,16 +9,11 @@ import React from "react";
|
|||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useDefaultEmail, VerifyCode } from "@/components/auth/auth-forms";
|
||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
||||
import { TextInput } from "@/components/text-input";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { useDayjs } from "@/utils/dayjs";
|
||||
import { requiredString, validEmail } from "@/utils/form-validation";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
import { AuthLayout } from "../components/auth/auth-layout";
|
||||
import { getStaticTranslations } from "../utils/with-page-translations";
|
||||
|
||||
type RegisterFormData = {
|
||||
name: string;
|
||||
email: string;
|
||||
|
@ -30,7 +25,8 @@ export const RegisterForm: React.FunctionComponent<{
|
|||
const [defaultEmail, setDefaultEmail] = useDefaultEmail();
|
||||
const { t } = useTranslation();
|
||||
const { timeZone } = useDayjs();
|
||||
const router = useRouter();
|
||||
const params = useParams<{ locale: string }>();
|
||||
const searchParams = useSearchParams();
|
||||
const { register, handleSubmit, getValues, setError, formState } =
|
||||
useForm<RegisterFormData>({
|
||||
defaultValues: { email: defaultEmail },
|
||||
|
@ -47,7 +43,7 @@ export const RegisterForm: React.FunctionComponent<{
|
|||
<VerifyCode
|
||||
onSubmit={async (code) => {
|
||||
// get user's time zone
|
||||
const locale = router.locale;
|
||||
const locale = params?.locale ?? "en";
|
||||
const res = await authenticateRegistration.mutateAsync({
|
||||
token,
|
||||
timeZone,
|
||||
|
@ -72,7 +68,7 @@ export const RegisterForm: React.FunctionComponent<{
|
|||
|
||||
signIn("registration-token", {
|
||||
token,
|
||||
callbackUrl: router.query.callbackUrl as string,
|
||||
callbackUrl: searchParams?.get("callbackUrl") ?? undefined,
|
||||
});
|
||||
}}
|
||||
onChange={() => setToken(undefined)}
|
||||
|
@ -181,24 +177,3 @@ export const RegisterForm: React.FunctionComponent<{
|
|||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Head>
|
||||
<title>{t("register")}</title>
|
||||
</Head>
|
||||
<RegisterForm />
|
||||
</AuthLayout>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = (page) => {
|
||||
return <StandardLayout hideNav={true}>{page}</StandardLayout>;
|
||||
};
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
export default Page;
|
27
apps/web/src/app/[locale]/admin/[adminUrlId]/page.tsx
Normal file
27
apps/web/src/app/[locale]/admin/[adminUrlId]/page.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { Redirect } from "@/app/components/redirect";
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
|
||||
import { PParams } from "./types";
|
||||
|
||||
export default async function Page({ params }: { params: PParams }) {
|
||||
const { adminUrlId } = params;
|
||||
|
||||
const poll = await prisma.poll.findUnique({
|
||||
where: { adminUrlId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!poll) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<Redirect
|
||||
from={absoluteUrl(`/admin/${params.adminUrlId}`)}
|
||||
href={absoluteUrl(`/poll/${poll.id}`)}
|
||||
/>
|
||||
);
|
||||
}
|
5
apps/web/src/app/[locale]/admin/[adminUrlId]/types.ts
Normal file
5
apps/web/src/app/[locale]/admin/[adminUrlId]/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Params } from "@/app/[locale]/types";
|
||||
|
||||
export interface PParams extends Params {
|
||||
adminUrlId: string;
|
||||
}
|
|
@ -1,34 +1,19 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { GetServerSideProps } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Skeleton } from "@/components/skeleton";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { UserAvatar } from "@/components/user";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
import { getServerSideTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
const params = z.object({
|
||||
magicLink: z.string().url(),
|
||||
});
|
||||
|
||||
const magicLinkParams = z.object({
|
||||
email: z.string().email(),
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
type PageProps = { magicLink: string; email: string };
|
||||
|
||||
const Page: NextPageWithLayout<PageProps> = ({ magicLink, email }) => {
|
||||
export const LoginPage = ({ magicLink, email }: PageProps) => {
|
||||
const session = useSession();
|
||||
const posthog = usePostHog();
|
||||
const trpcUtils = trpc.useUtils();
|
||||
|
@ -61,12 +46,8 @@ const Page: NextPageWithLayout<PageProps> = ({ magicLink, email }) => {
|
|||
});
|
||||
const { data } = trpc.user.getByEmail.useQuery({ email });
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center gap-4 p-4">
|
||||
<Head>
|
||||
<title>{t("login")}</title>
|
||||
</Head>
|
||||
<div className="mb-6">
|
||||
<Logo />
|
||||
</div>
|
||||
|
@ -105,43 +86,3 @@ const Page: NextPageWithLayout<PageProps> = ({ magicLink, email }) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = (page) => (
|
||||
<StandardLayout hideNav={true}>{page}</StandardLayout>
|
||||
);
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<PageProps> = async (
|
||||
ctx,
|
||||
) => {
|
||||
const parse = params.safeParse(ctx.query);
|
||||
|
||||
if (!parse.success) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const { magicLink } = parse.data;
|
||||
|
||||
const url = new URL(magicLink);
|
||||
|
||||
const parseMagicLink = magicLinkParams.safeParse(
|
||||
Object.fromEntries(url.searchParams),
|
||||
);
|
||||
|
||||
if (!parseMagicLink.success) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
magicLink,
|
||||
email: parseMagicLink.data.email,
|
||||
...(await getServerSideTranslations(ctx)),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default Page;
|
33
apps/web/src/app/[locale]/auth/login/not-found.tsx
Normal file
33
apps/web/src/app/[locale]/auth/login/not-found.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Button } from "@rallly/ui/button";
|
||||
import { XCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
import {
|
||||
PageDialog,
|
||||
PageDialogDescription,
|
||||
PageDialogFooter,
|
||||
PageDialogHeader,
|
||||
PageDialogTitle,
|
||||
} from "@/components/page-dialog";
|
||||
|
||||
export default async function NotFound() {
|
||||
// TODO (Luke Vella) [2023-10-31]: No way to get locale from not-found
|
||||
// See: https://github.com/vercel/next.js/discussions/43179
|
||||
const { t } = await getTranslation("en");
|
||||
return (
|
||||
<PageDialog icon={XCircleIcon}>
|
||||
<PageDialogHeader>
|
||||
<PageDialogTitle>{t("authErrorTitle")}</PageDialogTitle>
|
||||
<PageDialogDescription>
|
||||
{t("authErrorDescription")}
|
||||
</PageDialogDescription>
|
||||
</PageDialogHeader>
|
||||
<PageDialogFooter>
|
||||
<Button asChild variant="primary">
|
||||
<Link href="/login">{t("authErrorCta")}</Link>
|
||||
</Button>
|
||||
</PageDialogFooter>
|
||||
</PageDialog>
|
||||
);
|
||||
}
|
56
apps/web/src/app/[locale]/auth/login/page.tsx
Normal file
56
apps/web/src/app/[locale]/auth/login/page.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
import { LoginPage } from "./login-page";
|
||||
|
||||
const searchParamsSchema = z.object({
|
||||
magicLink: z.string().url(),
|
||||
});
|
||||
|
||||
type SearchParams = z.infer<typeof searchParamsSchema>;
|
||||
|
||||
const magicLinkParams = z.object({
|
||||
email: z.string().email(),
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParams;
|
||||
}) {
|
||||
const parse = searchParamsSchema.safeParse(searchParams);
|
||||
|
||||
if (!parse.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { magicLink } = parse.data;
|
||||
|
||||
const url = new URL(magicLink);
|
||||
|
||||
const parseMagicLink = magicLinkParams.safeParse(
|
||||
Object.fromEntries(url.searchParams),
|
||||
);
|
||||
|
||||
if (!parseMagicLink.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { email } = parseMagicLink.data;
|
||||
|
||||
return <LoginPage magicLink={magicLink} email={email} />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("login"),
|
||||
};
|
||||
}
|
93
apps/web/src/app/[locale]/invite/[urlId]/layout.tsx
Normal file
93
apps/web/src/app/[locale]/invite/[urlId]/layout.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<svg
|
||||
className="absolute inset-x-0 top-0 z-10 hidden h-[64rem] w-full stroke-gray-300/75 [mask-image:radial-gradient(800px_800px_at_center,white,transparent)] sm:block"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84"
|
||||
width={240}
|
||||
height={240}
|
||||
x="50%"
|
||||
y={-1}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path d="M.5 240V.5H240" fill="none" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
strokeWidth={0}
|
||||
fill="url(#1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="relative z-20">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { urlId, locale },
|
||||
}: {
|
||||
params: {
|
||||
urlId: string;
|
||||
locale: string;
|
||||
};
|
||||
}) {
|
||||
const poll = await prisma.poll.findUnique({
|
||||
where: {
|
||||
id: urlId as string,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = await getTranslation(locale);
|
||||
|
||||
if (!poll) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { title, id, user } = poll;
|
||||
|
||||
const name = user?.name ?? t("guest");
|
||||
|
||||
return {
|
||||
title,
|
||||
metadataBase: new URL(absoluteUrl()),
|
||||
openGraph: {
|
||||
title,
|
||||
description: `By ${name}`,
|
||||
url: `/invite/${id}`,
|
||||
images: [
|
||||
{
|
||||
url: `${absoluteUrl("/api/og-image-poll", {
|
||||
title,
|
||||
author: user?.name ?? t("guest"),
|
||||
})}`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title,
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies Metadata;
|
||||
}
|
114
apps/web/src/app/[locale]/invite/[urlId]/page.tsx
Normal file
114
apps/web/src/app/[locale]/invite/[urlId]/page.tsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { ArrowUpLeftIcon } from "lucide-react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { Poll } from "@/components/poll";
|
||||
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { UserDropdown } from "@/components/user-dropdown";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
import { VisibilityProvider } from "@/components/visibility";
|
||||
import { PermissionsContext } from "@/contexts/permissions";
|
||||
import { usePoll } from "@/contexts/poll";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
const Prefetch = ({ children }: React.PropsWithChildren) => {
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams?.get("token") as string;
|
||||
const params = useParams<{ urlId: string }>();
|
||||
const urlId = params?.urlId as string;
|
||||
const { data: permission } = trpc.auth.getUserPermission.useQuery(
|
||||
{ token },
|
||||
{
|
||||
enabled: !!token,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: poll, error } = trpc.polls.get.useQuery(
|
||||
{ urlId },
|
||||
{
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: participants } = trpc.polls.participants.list.useQuery({
|
||||
pollId: urlId,
|
||||
});
|
||||
|
||||
if (error?.data?.code === "NOT_FOUND") {
|
||||
return <div>Not found</div>;
|
||||
}
|
||||
if (!poll || !participants) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PermissionsContext.Provider value={{ userId: permission?.userId ?? null }}>
|
||||
<Head>
|
||||
<title>{poll.title}</title>
|
||||
</Head>
|
||||
{children}
|
||||
</PermissionsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const GoToApp = () => {
|
||||
const poll = usePoll();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 p-3">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className={poll.userId !== user.id ? "hidden" : ""}
|
||||
>
|
||||
<Link href={`/poll/${poll.id}`}>
|
||||
<ArrowUpLeftIcon className="h-4 w-4" />
|
||||
<Trans i18nKey="manage" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function InvitePage() {
|
||||
return (
|
||||
<Prefetch>
|
||||
<LegacyPollContextProvider>
|
||||
<VisibilityProvider>
|
||||
<GoToApp />
|
||||
<div className="mx-auto max-w-4xl space-y-4 px-3 sm:py-8">
|
||||
<Poll />
|
||||
<div className="mt-4 space-y-4 text-center text-gray-500">
|
||||
<div className="py-8">
|
||||
<Trans
|
||||
defaults="Powered by <a>{name}</a>"
|
||||
i18nKey="poweredByRallly"
|
||||
values={{ name: "rallly.co" }}
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
className="hover:text-primary-600 rounded-none border-b border-b-gray-500 font-semibold"
|
||||
href="https://rallly.co"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VisibilityProvider>
|
||||
</LegacyPollContextProvider>
|
||||
</Prefetch>
|
||||
);
|
||||
}
|
33
apps/web/src/app/[locale]/layout.tsx
Normal file
33
apps/web/src/app/[locale]/layout.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import "tailwindcss/tailwind.css";
|
||||
import "../../style.css";
|
||||
|
||||
import languages from "@rallly/languages";
|
||||
import { Inter } from "next/font/google";
|
||||
import React from "react";
|
||||
|
||||
import { Providers } from "@/app/providers";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(languages).map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export default function Root({
|
||||
children,
|
||||
params: { locale },
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}) {
|
||||
return (
|
||||
<html lang={locale} className={inter.className}>
|
||||
<body className="h-screen overflow-y-scroll">
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
10
apps/web/src/app/[locale]/logout/page.tsx
Normal file
10
apps/web/src/app/[locale]/logout/page.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
"use client";
|
||||
import { signOut } from "next-auth/react";
|
||||
import React from "react";
|
||||
|
||||
export default function Page() {
|
||||
React.useEffect(() => {
|
||||
signOut({ callbackUrl: "/login" });
|
||||
});
|
||||
return null;
|
||||
}
|
39
apps/web/src/app/[locale]/not-found.tsx
Normal file
39
apps/web/src/app/[locale]/not-found.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Button } from "@rallly/ui/button";
|
||||
import { FileSearchIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
export default async function Page() {
|
||||
// TODO (Luke Vella) [2023-11-03]: not-found doesn't have access to params right now
|
||||
// See: https://github.com/vercel/next.js/discussions/43179
|
||||
const { t } = await getTranslation("en");
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-100px)] w-full items-center justify-center">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4 text-center">
|
||||
<FileSearchIcon className="mb-4 inline-block h-24 w-24 text-gray-400" />
|
||||
<div className="text-primary-600 mb-2 text-3xl font-bold ">
|
||||
{t("errors_notFoundTitle")}
|
||||
</div>
|
||||
<p className="text-gray-600">{t("errors_notFoundDescription")}</p>
|
||||
</div>
|
||||
<div className="flex justify-center space-x-3">
|
||||
<Button variant="primary" asChild>
|
||||
<Link href="/">{t("errors_goToHome")}</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link
|
||||
href="https://support.rallly.co"
|
||||
passHref={true}
|
||||
className="btn-default"
|
||||
>
|
||||
{t("common_support")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
27
apps/web/src/app/[locale]/p/[participantUrlId]/page.tsx
Normal file
27
apps/web/src/app/[locale]/p/[participantUrlId]/page.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { Redirect } from "@/app/components/redirect";
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
|
||||
import { PParams } from "./types";
|
||||
|
||||
export default async function Page({ params }: { params: PParams }) {
|
||||
const { participantUrlId } = params;
|
||||
|
||||
const poll = await prisma.poll.findUnique({
|
||||
where: { participantUrlId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!poll) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<Redirect
|
||||
from={absoluteUrl(`/p/${params.participantUrlId}`)}
|
||||
href={absoluteUrl(`/invite/${poll.id}`)}
|
||||
/>
|
||||
);
|
||||
}
|
5
apps/web/src/app/[locale]/p/[participantUrlId]/types.ts
Normal file
5
apps/web/src/app/[locale]/p/[participantUrlId]/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Params } from "@/app/[locale]/types";
|
||||
|
||||
export interface PParams extends Params {
|
||||
participantUrlId: string;
|
||||
}
|
3
apps/web/src/app/[locale]/types.ts
Normal file
3
apps/web/src/app/[locale]/types.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export interface Params {
|
||||
locale: string;
|
||||
}
|
32
apps/web/src/app/components/redirect.tsx
Normal file
32
apps/web/src/app/components/redirect.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTimeoutFn } from "react-use";
|
||||
|
||||
import { PageDialog, PageDialogDescription } from "@/components/page-dialog";
|
||||
import { Trans } from "@/components/trans";
|
||||
|
||||
export function Redirect({ href }: { from: string; href: string }) {
|
||||
const router = useRouter();
|
||||
useTimeoutFn(() => {
|
||||
router.push(href);
|
||||
}, 3000);
|
||||
|
||||
return (
|
||||
<PageDialog>
|
||||
<PageDialogDescription>
|
||||
<Trans
|
||||
i18nKey="pageMovedDescription"
|
||||
defaults="Redirecting to <a>{newUrl}</a>"
|
||||
values={{
|
||||
newUrl: href,
|
||||
}}
|
||||
components={{
|
||||
a: <Link className="text-link" href={href} />,
|
||||
}}
|
||||
/>
|
||||
</PageDialogDescription>
|
||||
</PageDialog>
|
||||
);
|
||||
}
|
30
apps/web/src/app/i18n.ts
Normal file
30
apps/web/src/app/i18n.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { createInstance, Namespace } from "i18next";
|
||||
import resourcesToBackend from "i18next-resources-to-backend";
|
||||
import { initReactI18next } from "react-i18next/initReactI18next";
|
||||
|
||||
import { defaultNS, getOptions } from "./i18n/settings";
|
||||
|
||||
const initI18next = async (lng: string, ns: Namespace) => {
|
||||
const i18nInstance = createInstance();
|
||||
await i18nInstance
|
||||
.use(initReactI18next)
|
||||
.use(
|
||||
resourcesToBackend(
|
||||
(language: string, namespace: string) =>
|
||||
import(`../../public/locales/${language}/${namespace}.json`),
|
||||
),
|
||||
)
|
||||
.init(getOptions(lng, ns));
|
||||
return i18nInstance;
|
||||
};
|
||||
|
||||
export async function getTranslation(
|
||||
locale: string,
|
||||
ns: Namespace = defaultNS,
|
||||
) {
|
||||
const i18nextInstance = await initI18next(locale, ns);
|
||||
return {
|
||||
t: i18nextInstance.getFixedT(locale, Array.isArray(ns) ? ns[0] : ns),
|
||||
i18n: i18nextInstance,
|
||||
};
|
||||
}
|
52
apps/web/src/app/i18n/client.tsx
Normal file
52
apps/web/src/app/i18n/client.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
"use client";
|
||||
import i18next, { Namespace } from "i18next";
|
||||
import ICU from "i18next-icu";
|
||||
import resourcesToBackend from "i18next-resources-to-backend";
|
||||
import { useParams } from "next/navigation";
|
||||
import React from "react";
|
||||
import {
|
||||
I18nextProvider,
|
||||
initReactI18next,
|
||||
useTranslation as useTranslationOrg,
|
||||
} from "react-i18next";
|
||||
import { useAsync } from "react-use";
|
||||
|
||||
import { defaultNS, getOptions } from "./settings";
|
||||
|
||||
async function initTranslations(lng: string) {
|
||||
const i18n = i18next
|
||||
.use(initReactI18next)
|
||||
.use(ICU)
|
||||
.use(
|
||||
resourcesToBackend(
|
||||
(language: string, namespace: string) =>
|
||||
import(`../../../public/locales/${language}/${namespace}.json`),
|
||||
),
|
||||
);
|
||||
await i18n.init(getOptions(lng));
|
||||
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export function useTranslation(ns?: Namespace) {
|
||||
return useTranslationOrg(ns);
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
const params = useParams<{ locale: string }>();
|
||||
const locale = params?.locale ?? defaultNS;
|
||||
|
||||
const res = useAsync(async () => {
|
||||
return await initTranslations(locale);
|
||||
});
|
||||
|
||||
if (!res.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<I18nextProvider i18n={res.value} defaultNS={defaultNS}>
|
||||
{children}
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
21
apps/web/src/app/i18n/settings.ts
Normal file
21
apps/web/src/app/i18n/settings.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import allLanguages from "@rallly/languages";
|
||||
import { InitOptions } from "i18next";
|
||||
|
||||
export const fallbackLng = "en";
|
||||
export const languages = Object.keys(allLanguages);
|
||||
export const defaultNS = "app";
|
||||
|
||||
export function getOptions(
|
||||
lng = fallbackLng,
|
||||
ns: string | string[] = defaultNS,
|
||||
): InitOptions {
|
||||
return {
|
||||
// debug: true,
|
||||
supportedLngs: languages,
|
||||
fallbackLng,
|
||||
lng,
|
||||
fallbackNS: defaultNS,
|
||||
defaultNS,
|
||||
ns,
|
||||
};
|
||||
}
|
46
apps/web/src/app/providers.tsx
Normal file
46
apps/web/src/app/providers.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
"use client";
|
||||
import { AppRouter } from "@rallly/backend/trpc/routers";
|
||||
import { TooltipProvider } from "@rallly/ui/tooltip";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
import { domMax, LazyMotion } from "framer-motion";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { I18nProvider } from "@/app/i18n/client";
|
||||
import { UserProvider } from "@/components/user-provider";
|
||||
import { ConnectedDayjsProvider } from "@/utils/dayjs";
|
||||
import { trpcConfig } from "@/utils/trpc/config";
|
||||
|
||||
export const trpc = createTRPCReact<AppRouter>({
|
||||
unstable_overrides: {
|
||||
useMutation: {
|
||||
async onSuccess(opts) {
|
||||
await opts.originalFn();
|
||||
await opts.queryClient.invalidateQueries();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function Providers(props: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
const [trpcClient] = useState(() => trpc.createClient(trpcConfig));
|
||||
return (
|
||||
<LazyMotion features={domMax}>
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider>
|
||||
<ConnectedDayjsProvider>
|
||||
<TooltipProvider>
|
||||
<SessionProvider>
|
||||
<UserProvider>{props.children}</UserProvider>
|
||||
</SessionProvider>
|
||||
</TooltipProvider>
|
||||
</ConnectedDayjsProvider>
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
</LazyMotion>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
@ -133,7 +134,7 @@ export const LoginForm: React.FunctionComponent<{
|
|||
const [email, setEmail] = React.useState<string>();
|
||||
const posthog = usePostHog();
|
||||
const router = useRouter();
|
||||
const callbackUrl = (router.query.callbackUrl as string) ?? "/";
|
||||
const callbackUrl = (useSearchParams()?.get("callbackUrl") as string) ?? "/";
|
||||
|
||||
const sendVerificationEmail = (email: string) => {
|
||||
return signIn("email", {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { TrendingUpIcon } from "@rallly/icons";
|
||||
import {
|
||||
BillingPlan,
|
||||
BillingPlanDescription,
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
} from "@rallly/ui/billing-plan";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
||||
import { TrendingUpIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
import { Trans } from "@/components/trans";
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { GlobeIcon } from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import {
|
||||
Dialog,
|
||||
|
@ -10,6 +9,7 @@ import {
|
|||
} from "@rallly/ui/dialog";
|
||||
import { Label } from "@rallly/ui/label";
|
||||
import dayjs from "dayjs";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useInterval } from "react-use";
|
||||
import spacetime from "spacetime";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
@ -7,7 +8,7 @@ import {
|
|||
CardTitle,
|
||||
} from "@rallly/ui/card";
|
||||
import { Form } from "@rallly/ui/form";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import useFormPersist from "react-hook-form-persist";
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
import {
|
||||
MessageCircleIcon,
|
||||
MoreHorizontalIcon,
|
||||
TrashIcon,
|
||||
} from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
@ -12,6 +7,7 @@ import {
|
|||
} from "@rallly/ui/dropdown-menu";
|
||||
import { Textarea } from "@rallly/ui/textarea";
|
||||
import dayjs from "dayjs";
|
||||
import { MessageCircleIcon, MoreHorizontalIcon, TrashIcon } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { FrownIcon } from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import Head from "next/head";
|
||||
import { FrownIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
@ -19,9 +18,6 @@ const ErrorPage: React.FunctionComponent<ComponentProps> = ({
|
|||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-100px)] w-full items-center justify-center">
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4 text-center">
|
||||
<Icon className="mb-4 inline-block h-24 w-24 text-gray-400" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { MapPinIcon, MousePointerClickIcon, TextIcon } from "@rallly/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { MapPinIcon, MousePointerClickIcon, TextIcon } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import { Card } from "@/components/card";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { HelpCircleIcon } from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||
import { HelpCircleIcon } from "lucide-react";
|
||||
import Script from "next/script";
|
||||
import React from "react";
|
||||
|
||||
|
|
|
@ -1,10 +1,3 @@
|
|||
import {
|
||||
BugIcon,
|
||||
LifeBuoyIcon,
|
||||
LightbulbIcon,
|
||||
MegaphoneIcon,
|
||||
SmileIcon,
|
||||
} from "@rallly/icons";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
@ -13,6 +6,13 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@rallly/ui/dropdown-menu";
|
||||
import {
|
||||
BugIcon,
|
||||
LifeBuoyIcon,
|
||||
LightbulbIcon,
|
||||
MegaphoneIcon,
|
||||
SmileIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ChevronLeftIcon, ChevronRightIcon } from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
||||
|
|
|
@ -1,12 +1,3 @@
|
|||
import {
|
||||
CalendarIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
SparklesIcon,
|
||||
XIcon,
|
||||
} from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
|
@ -20,6 +11,15 @@ import {
|
|||
import { Switch } from "@rallly/ui/switch";
|
||||
import clsx from "clsx";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
SparklesIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
||||
|
|
|
@ -6,9 +6,9 @@ import {
|
|||
useFloating,
|
||||
} from "@floating-ui/react-dom-interactions";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
import { ChevronDownIcon } from "@rallly/icons";
|
||||
import clsx from "clsx";
|
||||
import dayjs from "dayjs";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { getDuration } from "@/utils/date-time-utils";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { CalendarIcon, TableIcon } from "@rallly/icons";
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from "@rallly/ui/card";
|
||||
import { FormField, FormMessage } from "@rallly/ui/form";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
||||
import { CalendarIcon, TableIcon } from "lucide-react";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { XIcon } from "@rallly/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { XIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Calendar } from "react-big-calendar";
|
||||
import { createBreakpoint } from "react-use";
|
||||
|
@ -98,7 +98,7 @@ const WeekCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
|||
width: `calc(${props.style?.width}%)`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-1.5 right-1.5 flex justify-end opacity-0 group-hover:opacity-100">
|
||||
<div className="absolute right-1.5 top-1.5 flex justify-end opacity-0 group-hover:opacity-100">
|
||||
<XIcon className="h-3 w-3" />
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -1,10 +1,3 @@
|
|||
import {
|
||||
AtSignIcon,
|
||||
EyeIcon,
|
||||
InfoIcon,
|
||||
MessageCircleIcon,
|
||||
VoteIcon,
|
||||
} from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import {
|
||||
Card,
|
||||
|
@ -16,6 +9,13 @@ import {
|
|||
import { FormField } from "@rallly/ui/form";
|
||||
import { Switch } from "@rallly/ui/switch";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||
import {
|
||||
AtSignIcon,
|
||||
EyeIcon,
|
||||
InfoIcon,
|
||||
MessageCircleIcon,
|
||||
VoteIcon,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { Trans } from "react-i18next";
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ArrowUpRightIcon, Share2Icon } from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
|
@ -8,6 +7,7 @@ import {
|
|||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@rallly/ui/dialog";
|
||||
import { ArrowUpRightIcon, Share2Icon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
const AdminLayout = (props: React.PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
|
||||
const [urlId] = React.useState(router.query.urlId as string);
|
||||
const { user } = useUser();
|
||||
const { data } = trpc.polls.get.useQuery({ urlId });
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.userId !== user.id) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
return <StandardLayout>{props.children}</StandardLayout>;
|
||||
};
|
||||
|
||||
export const getAdminLayout: NextPageWithLayout["getLayout"] =
|
||||
function getLayout(page) {
|
||||
return <AdminLayout>{page}</AdminLayout>;
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
import { NextSeo } from "next-seo";
|
||||
|
||||
export const AuthLayout = (
|
||||
props: React.PropsWithChildren<{ title: string }>,
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<NextSeo nofollow={true} title={props.title} />
|
||||
<div className="flex h-screen items-center justify-center p-4">
|
||||
<div className="space-y-2 rounded-md border bg-white p-6 text-center shadow-sm">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,3 +1,12 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuItemIconLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@rallly/ui/dropdown-menu";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowUpRight,
|
||||
|
@ -9,18 +18,10 @@ import {
|
|||
PlayCircleIcon,
|
||||
RotateCcw,
|
||||
ShieldCloseIcon,
|
||||
} from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuItemIconLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@rallly/ui/dropdown-menu";
|
||||
} from "lucide-react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
|
@ -46,7 +47,6 @@ import { Skeleton } from "@/components/skeleton";
|
|||
import { Trans } from "@/components/trans";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
import { usePoll } from "@/contexts/poll";
|
||||
import Error404 from "@/pages/404";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
import { NextPageWithLayout } from "../../types";
|
||||
|
@ -155,12 +155,12 @@ const StatusControl = () => {
|
|||
const AdminControls = () => {
|
||||
const poll = usePoll();
|
||||
const pollLink = `/poll/${poll.id}`;
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<TopBar>
|
||||
<div className="flex flex-col items-start justify-between gap-x-4 gap-y-2 sm:flex-row">
|
||||
<div className="flex min-w-0 gap-4">
|
||||
{router.asPath !== pollLink ? (
|
||||
{pathname !== pollLink ? (
|
||||
<Button asChild>
|
||||
<Link href={pollLink}>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
|
@ -257,18 +257,14 @@ const Title = () => {
|
|||
};
|
||||
|
||||
const Prefetch = ({ children }: React.PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
const urlId = router.query.urlId as string;
|
||||
const urlId = params?.urlId as string;
|
||||
|
||||
const poll = trpc.polls.get.useQuery({ urlId });
|
||||
const participants = trpc.polls.participants.list.useQuery({ pollId: urlId });
|
||||
const watchers = trpc.polls.getWatchers.useQuery({ pollId: urlId });
|
||||
|
||||
if (poll.error?.data?.code === "NOT_FOUND") {
|
||||
return <Error404 />;
|
||||
}
|
||||
|
||||
if (!poll.data || !watchers.data || !participants.data) {
|
||||
return (
|
||||
<div>
|
||||
|
@ -287,10 +283,10 @@ const Prefetch = ({ children }: React.PropsWithChildren) => {
|
|||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const PollLayout = ({ children }: React.PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
export const PollLayout = ({ children }: React.PropsWithChildren) => {
|
||||
const params = useParams();
|
||||
|
||||
const urlId = router.query.urlId as string;
|
||||
const urlId = params?.urlId as string;
|
||||
|
||||
if (!urlId) {
|
||||
// probably navigating away
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import { cn } from "@rallly/ui";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Card } from "@rallly/ui/card";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
CreditCardIcon,
|
||||
MenuIcon,
|
||||
Settings2Icon,
|
||||
UserIcon,
|
||||
} from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Card } from "@rallly/ui/card";
|
||||
import clsx from "clsx";
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useToggle } from "react-use";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
||||
import { IfCloudHosted } from "@/contexts/environment";
|
||||
import { Plan } from "@/contexts/plan";
|
||||
|
||||
import { IconComponent, NextPageWithLayout } from "../../types";
|
||||
import { IconComponent } from "../../types";
|
||||
import { IfAuthenticated, useUser } from "../user-provider";
|
||||
|
||||
const MenuItem = (props: {
|
||||
|
@ -27,12 +26,12 @@ const MenuItem = (props: {
|
|||
href: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<Link
|
||||
className={clsx(
|
||||
"flex min-w-0 items-center gap-x-2.5 px-2.5 py-1.5 text-sm font-medium",
|
||||
router.asPath === props.href
|
||||
pathname === props.href
|
||||
? "bg-gray-200"
|
||||
: "text-gray-500 hover:bg-gray-100 hover:text-gray-800",
|
||||
)}
|
||||
|
@ -48,13 +47,13 @@ export const ProfileLayout = ({ children }: React.PropsWithChildren) => {
|
|||
const { user } = useUser();
|
||||
|
||||
// reset toggle whenever route changes
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [isMenuOpen, toggle] = useToggle(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
toggle(false);
|
||||
}, [router.asPath, toggle]);
|
||||
}, [pathname, toggle]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -101,12 +100,3 @@ export const ProfileLayout = ({ children }: React.PropsWithChildren) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getProfileLayout: NextPageWithLayout["getLayout"] =
|
||||
function getLayout(page) {
|
||||
return (
|
||||
<StandardLayout>
|
||||
<ProfileLayout>{page}</ProfileLayout>
|
||||
</StandardLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { ListIcon, SparklesIcon } from "@rallly/icons";
|
||||
"use client";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import clsx from "clsx";
|
||||
import { AnimatePresence, m } from "framer-motion";
|
||||
import { ListIcon, SparklesIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
|
@ -17,7 +18,6 @@ import {
|
|||
import FeedbackButton from "@/components/feedback";
|
||||
import { LoginLink } from "@/components/login-link";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { UserDropdown } from "@/components/user-dropdown";
|
||||
import { IfCloudHosted } from "@/contexts/environment";
|
||||
|
@ -41,14 +41,14 @@ const NavMenuItem = ({
|
|||
label: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<Button variant="ghost" asChild>
|
||||
<Link
|
||||
target={target}
|
||||
href={href}
|
||||
className={cn(
|
||||
router.asPath === href
|
||||
pathname === href
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground active:bg-gray-200/50",
|
||||
className,
|
||||
|
@ -78,18 +78,6 @@ const Upgrade = () => {
|
|||
};
|
||||
|
||||
const LogoArea = () => {
|
||||
const router = useRouter();
|
||||
const [isBusy, setIsBusy] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
const setBusy = () => setIsBusy(true);
|
||||
const setNotBusy = () => setIsBusy(false);
|
||||
router.events.on("routeChangeStart", setBusy);
|
||||
router.events.on("routeChangeComplete", setNotBusy);
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", setBusy);
|
||||
router.events.off("routeChangeComplete", setNotBusy);
|
||||
};
|
||||
}, [router.events]);
|
||||
return (
|
||||
<div className="relative flex items-center justify-center gap-x-4">
|
||||
<Link
|
||||
|
@ -100,14 +88,6 @@ const LogoArea = () => {
|
|||
>
|
||||
<Logo size="sm" />
|
||||
</Link>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none flex w-5 items-center justify-center text-gray-500 transition-opacity delay-500",
|
||||
isBusy ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
{isBusy ? <Spinner /> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Link, { LinkProps } from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
export const LoginLink = React.forwardRef<
|
||||
|
@ -7,6 +7,7 @@ export const LoginLink = React.forwardRef<
|
|||
React.PropsWithChildren<Omit<LinkProps, "href"> & { className?: string }>
|
||||
>(function LoginLink({ children, ...props }, ref) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname() ?? "/";
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
|
@ -15,7 +16,7 @@ export const LoginLink = React.forwardRef<
|
|||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
props.onClick?.(e);
|
||||
router.push("/login?callbackUrl=" + encodeURIComponent(router.asPath));
|
||||
router.push("/login?callbackUrl=" + encodeURIComponent(pathname));
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Dialog } from "@headlessui/react";
|
||||
import { XIcon } from "@rallly/icons";
|
||||
import { AnimatePresence, m } from "framer-motion";
|
||||
import { XIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { ButtonProps, LegacyButton } from "../button";
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { PencilIcon, TagIcon, TrashIcon } from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
|
@ -27,6 +26,7 @@ import {
|
|||
FormMessage,
|
||||
} from "@rallly/ui/form";
|
||||
import { Input } from "@rallly/ui/input";
|
||||
import { PencilIcon, TagIcon, TrashIcon } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
import { cn } from "@rallly/ui";
|
||||
import { Badge } from "@rallly/ui/badge";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
||||
import { m } from "framer-motion";
|
||||
import {
|
||||
CalendarCheck2Icon,
|
||||
CopyIcon,
|
||||
|
@ -7,14 +12,9 @@ import {
|
|||
LockIcon,
|
||||
Settings2Icon,
|
||||
TrendingUpIcon,
|
||||
} from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Badge } from "@rallly/ui/badge";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
||||
import { m } from "framer-motion";
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useParams } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { Trans } from "@/components/trans";
|
||||
|
@ -51,7 +51,7 @@ const Feature = ({
|
|||
};
|
||||
|
||||
const Teaser = () => {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
const [tab, setTab] = React.useState("yearly");
|
||||
|
||||
|
@ -213,7 +213,7 @@ const Teaser = () => {
|
|||
<Trans i18nKey="upgrade" defaults="Upgrade" />
|
||||
</UpgradeButton>
|
||||
<Button asChild className="w-full">
|
||||
<Link href={`/poll/${router.query.urlId as string}`}>
|
||||
<Link href={`/poll/${params?.urlId as string}`}>
|
||||
<Trans i18nKey="notToday" defaults="Not Today" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Participant, VoteType } from "@rallly/database";
|
||||
import { TrashIcon } from "@rallly/icons";
|
||||
import { keyBy } from "lodash";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CheckCircleIcon, PauseCircleIcon, RadioIcon } from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { CheckCircleIcon, PauseCircleIcon, RadioIcon } from "lucide-react";
|
||||
|
||||
import { Trans } from "@/components/trans";
|
||||
import { IconComponent } from "@/types";
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { cn } from "@rallly/ui";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
|
@ -5,10 +8,7 @@ import {
|
|||
PlusIcon,
|
||||
ShrinkIcon,
|
||||
Users2Icon,
|
||||
} from "@rallly/icons";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||
} from "lucide-react";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
import { RemoveScroll } from "react-remove-scroll";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Participant, VoteType } from "@rallly/database";
|
||||
import { MoreHorizontalIcon } from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import clsx from "clsx";
|
||||
import { MoreHorizontalIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { ParticipantDropdown } from "@/components/participant-dropdown";
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue