mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-10 15:36:49 +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
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||||
steps:
|
steps:
|
||||||
# Downloads a copy of the code in your repository before running CI tests
|
# Downloads a copy of the code in your repository before running CI tests
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
|
@ -56,21 +56,21 @@ jobs:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
|
||||||
- name: Set environment variables
|
|
||||||
run: |
|
|
||||||
echo "DATABASE_URL=postgresql://postgres:password@localhost:5432/rallly" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Set environment variables
|
||||||
|
run: |
|
||||||
|
echo "DATABASE_URL=postgresql://postgres:password@localhost:5450/db" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Run db
|
- name: Run db
|
||||||
run: |
|
run: |
|
||||||
docker pull postgres:14.2
|
docker pull postgres:14.2
|
||||||
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_DB=rallly postgres:14.2
|
docker run -d -p 5450:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_DB=rallly postgres:14.2
|
||||||
yarn wait-on --timeout 60000 tcp:localhost:5432
|
yarn wait-on --timeout 60000 tcp:localhost:5450
|
||||||
|
|
||||||
- name: Deploy migrations
|
- name: Deploy migrations
|
||||||
run: yarn db:deploy
|
run: yarn db:setup
|
||||||
|
|
||||||
- name: Install playwright dependencies
|
- name: Install playwright dependencies
|
||||||
run: yarn playwright install --with-deps chromium
|
run: yarn playwright install --with-deps chromium
|
||||||
|
|
28
README.md
28
README.md
|
@ -24,43 +24,51 @@ 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.
|
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
|
```bash
|
||||||
git clone https://github.com/lukevella/rallly.git
|
git clone https://github.com/lukevella/rallly.git
|
||||||
cd rallly
|
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
|
```bash
|
||||||
cp sample.env .env
|
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).
|
||||||
|
|
||||||
|
4. Setup the database
|
||||||
|
|
||||||
|
If you don't have a postgres database running locally, you can spin up a new database using docker by running:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn db:generate && yarn db:reset
|
yarn dx
|
||||||
|
```
|
||||||
|
|
||||||
|
If you already have a postgres database, you can run the migrations and seed the database by running:
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn db:setup
|
||||||
```
|
```
|
||||||
|
|
||||||
This will:
|
This will:
|
||||||
|
|
||||||
- generate the prisma database client
|
|
||||||
- run migrations to create the database schema
|
- run migrations to create the database schema
|
||||||
- seed the database with some random data
|
- seed the database with test users and random data
|
||||||
|
|
||||||
Start the Next.js server
|
5. Start the Next.js server
|
||||||
|
|
||||||
```
|
```
|
||||||
# For development
|
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
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",
|
"name": "@rallly/docs",
|
||||||
"version": "0.0.0",
|
"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",
|
"@svgr/webpack": "^6.5.1",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@vercel/analytics": "^0.1.8",
|
"@vercel/analytics": "^0.1.8",
|
||||||
"@vercel/og": "^0.5.11",
|
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"class-variance-authority": "^0.6.0",
|
"class-variance-authority": "^0.6.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
|
@ -50,15 +49,8 @@
|
||||||
"@types/smoothscroll-polyfill": "^0.3.1",
|
"@types/smoothscroll-polyfill": "^0.3.1",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"cross-env": "^7.0.3",
|
"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": "^4.2.0",
|
||||||
"i18next-scanner-typescript": "^1.1.1",
|
"i18next-scanner-typescript": "^1.1.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.8",
|
|
||||||
"smtp-tester": "^2.0.1",
|
"smtp-tester": "^2.0.1",
|
||||||
"wait-on": "^6.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 Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
|
import { m } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
CalendarCheck2Icon,
|
CalendarCheck2Icon,
|
||||||
LanguagesIcon,
|
LanguagesIcon,
|
||||||
Users2Icon,
|
Users2Icon,
|
||||||
ZapIcon,
|
ZapIcon,
|
||||||
} from "@rallly/icons";
|
} from "lucide-react";
|
||||||
import { cn } from "@rallly/ui";
|
|
||||||
import { m } from "framer-motion";
|
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { IconComponent } from "@/types";
|
import { IconComponent } from "@/types";
|
||||||
|
|
|
@ -40,7 +40,7 @@ function isBright(color: RGBColor): boolean {
|
||||||
export const getRandomAvatarColor = (str: string) => {
|
export const getRandomAvatarColor = (str: string) => {
|
||||||
const strSum = str.split("").reduce((acc, val) => acc + val.charCodeAt(0), 0);
|
const strSum = str.split("").reduce((acc, val) => acc + val.charCodeAt(0), 0);
|
||||||
const randomIndex = strSum % avatarBackgroundColors.length;
|
const randomIndex = strSum % avatarBackgroundColors.length;
|
||||||
const color = avatarBackgroundColors[randomIndex];
|
const color = avatarBackgroundColors[randomIndex] as RGBColor;
|
||||||
const [r, g, b] = color;
|
const [r, g, b] = color;
|
||||||
return { color: `rgb(${r}, ${g}, ${b})`, requiresDarkText: isBright(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 { Badge } from "@rallly/ui/badge";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { preventWidows } from "@rallly/utils";
|
import { preventWidows } from "@rallly/utils";
|
||||||
import { m } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
|
import { ChevronRightIcon } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import * as React from "react";
|
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 Script from "next/script";
|
||||||
|
|
||||||
import PageLayout from "@/components/layouts/page-layout";
|
import PageLayout from "@/components/layouts/page-layout";
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { ChevronRightIcon, MenuIcon } from "@rallly/icons";
|
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
@ -7,6 +6,7 @@ import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@rallly/ui/dropdown-menu";
|
} from "@rallly/ui/dropdown-menu";
|
||||||
|
import { ChevronRightIcon, MenuIcon } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { DiscordIcon } from "@rallly/icons";
|
||||||
DiscordIcon,
|
|
||||||
GithubIcon,
|
|
||||||
LanguagesIcon,
|
|
||||||
LinkedinIcon,
|
|
||||||
TwitterIcon,
|
|
||||||
} from "@rallly/icons";
|
|
||||||
import languages from "@rallly/languages";
|
import languages from "@rallly/languages";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
@ -13,6 +7,12 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@rallly/ui/select";
|
} from "@rallly/ui/select";
|
||||||
|
import {
|
||||||
|
GithubIcon,
|
||||||
|
LanguagesIcon,
|
||||||
|
LinkedinIcon,
|
||||||
|
TwitterIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ArrowUpRight } from "@rallly/icons";
|
|
||||||
import { m } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
|
import { ArrowUpRight } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { NextSeo } from "next-seo";
|
import { NextSeo } from "next-seo";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FileSearchIcon } from "@rallly/icons";
|
import { FileSearchIcon } from "lucide-react";
|
||||||
import { GetStaticProps } from "next";
|
import { GetStaticProps } from "next";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { ImageResponse } from "@vercel/og";
|
import { ImageResponse } from "next/og";
|
||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ArrowLeftIcon } from "@rallly/icons";
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
import { GetStaticPropsContext } from "next";
|
import { GetStaticPropsContext } from "next";
|
||||||
import ErrorPage from "next/error";
|
import ErrorPage from "next/error";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
@ -117,6 +117,8 @@ export async function getStaticProps(ctx: GetStaticPropsContext) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { TrendingUpIcon } from "@rallly/icons";
|
|
||||||
import {
|
import {
|
||||||
BillingPlan,
|
BillingPlan,
|
||||||
BillingPlanDescription,
|
BillingPlanDescription,
|
||||||
|
@ -11,6 +10,7 @@ import {
|
||||||
} from "@rallly/ui/billing-plan";
|
} from "@rallly/ui/billing-plan";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
||||||
|
import { TrendingUpIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { NextSeo } from "next-seo";
|
import { NextSeo } from "next-seo";
|
||||||
|
|
|
@ -1,24 +1,13 @@
|
||||||
{
|
{
|
||||||
|
"extends": "@rallly/tsconfig/next.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
"~/*": ["public/*"]
|
"~/*": ["public/*"]
|
||||||
},
|
},
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"checkJs": false
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"incremental": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve"
|
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next-i18next.config.js"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules", "**/*.js"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,5 @@ PORT=3002
|
||||||
NEXT_PUBLIC_BASE_URL=http://localhost:3002
|
NEXT_PUBLIC_BASE_URL=http://localhost:3002
|
||||||
NEXTAUTH_URL=http://localhost:3002
|
NEXTAUTH_URL=http://localhost:3002
|
||||||
SECRET_PASSWORD=abcdefghijklmnopqrstuvwxyz1234567890
|
SECRET_PASSWORD=abcdefghijklmnopqrstuvwxyz1234567890
|
||||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/rallly
|
DATABASE_URL=postgres://postgres:postgres@localhost:5450/db
|
||||||
SUPPORT_EMAIL=support@rallly.co
|
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";
|
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" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
/// <reference types="next/navigation-types/compat/navigation" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
|
|
|
@ -4,17 +4,16 @@
|
||||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
const { withSentryConfig } = require("@sentry/nextjs");
|
const { withSentryConfig } = require("@sentry/nextjs");
|
||||||
const i18n = require("./i18n.config.js");
|
|
||||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||||
enabled: process.env.ANALYZE === "true",
|
enabled: process.env.ANALYZE === "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
i18n: { ...i18n, localeDetection: false },
|
|
||||||
productionBrowserSourceMaps: true,
|
productionBrowserSourceMaps: true,
|
||||||
output: "standalone",
|
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@rallly/backend",
|
"@rallly/backend",
|
||||||
|
"@rallly/database",
|
||||||
"@rallly/icons",
|
"@rallly/icons",
|
||||||
"@rallly/ui",
|
"@rallly/ui",
|
||||||
"@rallly/tailwind-config",
|
"@rallly/tailwind-config",
|
||||||
|
@ -22,12 +21,14 @@ const nextConfig = {
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
issuer: /\.[jt]sx?$/,
|
|
||||||
use: ["@svgr/webpack"],
|
use: ["@svgr/webpack"],
|
||||||
});
|
});
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
|
@ -61,6 +62,7 @@ const sentryWebpackPluginOptions = {
|
||||||
// recommended:
|
// recommended:
|
||||||
// release, url, org, project, authToken, configFile, stripPrefix,
|
// release, url, org, project, authToken, configFile, stripPrefix,
|
||||||
// urlPrefix, include, ignore
|
// urlPrefix, include, ignore
|
||||||
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
dryRun: !process.env.SENTRY_AUTH_TOKEN,
|
dryRun: !process.env.SENTRY_AUTH_TOKEN,
|
||||||
silent: true, // Suppresses all logs
|
silent: true, // Suppresses all logs
|
||||||
// For all available options, see:
|
// For all available options, see:
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env TAILWIND_MODE=watch next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
"build:test": "dotenv -e .env.test -- next build",
|
||||||
"analyze": "cross-env ANALYZE=true next build",
|
"analyze": "cross-env ANALYZE=true next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
@ -29,7 +30,6 @@
|
||||||
"@rallly/languages": "*",
|
"@rallly/languages": "*",
|
||||||
"@rallly/tailwind-config": "*",
|
"@rallly/tailwind-config": "*",
|
||||||
"@rallly/ui": "*",
|
"@rallly/ui": "*",
|
||||||
"@sentry/nextjs": "^7.74.1",
|
|
||||||
"@svgr/webpack": "^6.5.1",
|
"@svgr/webpack": "^6.5.1",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@tanstack/react-query": "^4.0.0",
|
"@tanstack/react-query": "^4.0.0",
|
||||||
|
@ -37,7 +37,6 @@
|
||||||
"@trpc/client": "^10.13.0",
|
"@trpc/client": "^10.13.0",
|
||||||
"@trpc/next": "^10.13.0",
|
"@trpc/next": "^10.13.0",
|
||||||
"@trpc/react-query": "^10.13.0",
|
"@trpc/react-query": "^10.13.0",
|
||||||
"@vercel/og": "^0.5.13",
|
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"class-variance-authority": "^0.6.0",
|
"class-variance-authority": "^0.6.0",
|
||||||
|
@ -48,6 +47,7 @@
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"i18next": "^22.4.9",
|
"i18next": "^22.4.9",
|
||||||
"i18next-icu": "^2.3.0",
|
"i18next-icu": "^2.3.0",
|
||||||
|
"i18next-resources-to-backend": "^1.1.4",
|
||||||
"ics": "^3.1.0",
|
"ics": "^3.1.0",
|
||||||
"intl-messageformat": "^10.3.4",
|
"intl-messageformat": "^10.3.4",
|
||||||
"iron-session": "^6.3.1",
|
"iron-session": "^6.3.1",
|
||||||
|
@ -72,11 +72,12 @@
|
||||||
"smoothscroll-polyfill": "^0.4.4",
|
"smoothscroll-polyfill": "^0.4.4",
|
||||||
"spacetime": "^7.4.7",
|
"spacetime": "^7.4.7",
|
||||||
"superjson": "^2.0.0",
|
"superjson": "^2.0.0",
|
||||||
"timezone-soft": "^1.4.1"
|
"timezone-soft": "^1.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.35.1",
|
"@playwright/test": "^1.39.0",
|
||||||
"@rallly/tsconfig": "*",
|
"@rallly/tsconfig": "*",
|
||||||
|
"@rallly/eslint-config": "*",
|
||||||
"@types/accept-language-parser": "^1.5.3",
|
"@types/accept-language-parser": "^1.5.3",
|
||||||
"@types/color-hash": "^1.0.2",
|
"@types/color-hash": "^1.0.2",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
|
@ -87,15 +88,8 @@
|
||||||
"@types/smoothscroll-polyfill": "^0.3.1",
|
"@types/smoothscroll-polyfill": "^0.3.1",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"cross-env": "^7.0.3",
|
"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": "^4.2.0",
|
||||||
"i18next-scanner-typescript": "^1.1.1",
|
"i18next-scanner-typescript": "^1.1.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.8",
|
|
||||||
"smtp-tester": "^2.0.1",
|
"smtp-tester": "^2.0.1",
|
||||||
"wait-on": "^6.0.1"
|
"wait-on": "^6.0.1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import path from "path";
|
||||||
|
|
||||||
const ci = process.env.CI === "true";
|
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
|
// Use process.env.PORT by default and fallback to port 3000
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
@ -24,13 +24,9 @@ const config: PlaywrightTestConfig = {
|
||||||
trace: "retain-on-failure",
|
trace: "retain-on-failure",
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
command: `NODE_ENV=test yarn dev --port ${PORT}`,
|
command: `NODE_ENV=test yarn start --port ${PORT}`,
|
||||||
url: baseURL,
|
url: baseURL,
|
||||||
timeout: 120 * 1000,
|
reuseExistingServer: !process.env.CI,
|
||||||
reuseExistingServer: !ci,
|
|
||||||
},
|
|
||||||
expect: {
|
|
||||||
timeout: 10000, // 10 seconds
|
|
||||||
},
|
},
|
||||||
reporter: [
|
reporter: [
|
||||||
[ci ? "github" : "list"],
|
[ci ? "github" : "list"],
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ["tailwindcss", "autoprefixer"],
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -225,5 +225,6 @@
|
||||||
"authErrorCta": "Go to login page",
|
"authErrorCta": "Go to login page",
|
||||||
"continueAs": "Continue as",
|
"continueAs": "Continue as",
|
||||||
"finalizeFeature": "Finalize",
|
"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 { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
|
@ -17,18 +18,16 @@ import {
|
||||||
} from "@rallly/ui/form";
|
} from "@rallly/ui/form";
|
||||||
import { Input } from "@rallly/ui/input";
|
import { Input } from "@rallly/ui/input";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
|
||||||
import { PayWall } from "@/components/pay-wall";
|
import { PayWall } from "@/components/pay-wall";
|
||||||
import { usePoll } from "@/components/poll-context";
|
import { usePoll } from "@/components/poll-context";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { NextPageWithLayout } from "@/types";
|
import { NextPageWithLayout } from "@/types";
|
||||||
import { usePostHog } from "@/utils/posthog";
|
import { usePostHog } from "@/utils/posthog";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
title: z.string().trim().min(1),
|
title: z.string().trim().min(1),
|
||||||
|
@ -63,7 +62,7 @@ const Page: NextPageWithLayout = () => {
|
||||||
pollId: poll.id,
|
pollId: poll.id,
|
||||||
newPollId: res.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;
|
export default Page;
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
@ -9,19 +10,17 @@ import {
|
||||||
} from "@rallly/ui/card";
|
} from "@rallly/ui/card";
|
||||||
import { Form } from "@rallly/ui/form";
|
import { Form } from "@rallly/ui/form";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PollDetailsData,
|
PollDetailsData,
|
||||||
PollDetailsForm,
|
PollDetailsForm,
|
||||||
} from "@/components/forms/poll-details-form";
|
} from "@/components/forms/poll-details-form";
|
||||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
|
||||||
import { useUpdatePollMutation } from "@/components/poll/mutations";
|
import { useUpdatePollMutation } from "@/components/poll/mutations";
|
||||||
import { usePoll } from "@/components/poll-context";
|
import { usePoll } from "@/components/poll-context";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { NextPageWithLayout } from "@/types";
|
import { NextPageWithLayout } from "@/types";
|
||||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
const Page: NextPageWithLayout = () => {
|
||||||
const { poll } = usePoll();
|
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;
|
export default Page;
|
|
@ -1,22 +1,20 @@
|
||||||
|
"use client";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { CardFooter } from "@rallly/ui/card";
|
import { CardFooter } from "@rallly/ui/card";
|
||||||
import { Form } from "@rallly/ui/form";
|
import { Form } from "@rallly/ui/form";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { PollOptionsData } from "@/components/forms";
|
import { PollOptionsData } from "@/components/forms";
|
||||||
import PollOptionsForm from "@/components/forms/poll-options-form";
|
import PollOptionsForm from "@/components/forms/poll-options-form";
|
||||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
|
||||||
import { useModalContext } from "@/components/modal/modal-provider";
|
import { useModalContext } from "@/components/modal/modal-provider";
|
||||||
import { useUpdatePollMutation } from "@/components/poll/mutations";
|
import { useUpdatePollMutation } from "@/components/poll/mutations";
|
||||||
import { usePoll } from "@/components/poll-context";
|
import { usePoll } from "@/components/poll-context";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { NextPageWithLayout } from "@/types";
|
|
||||||
import { encodeDateOption } from "@/utils/date-time-utils";
|
import { encodeDateOption } from "@/utils/date-time-utils";
|
||||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
|
||||||
|
|
||||||
const convertOptionToString = (option: { start: Date; duration: number }) => {
|
const convertOptionToString = (option: { start: Date; duration: number }) => {
|
||||||
const start = dayjs(option.start).utc();
|
const start = dayjs(option.start).utc();
|
||||||
|
@ -27,7 +25,7 @@ const convertOptionToString = (option: { start: Date; duration: number }) => {
|
||||||
.format("YYYY-MM-DDTHH:mm:ss")}`;
|
.format("YYYY-MM-DDTHH:mm:ss")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
const Page = () => {
|
||||||
const { poll, getParticipantsWhoVotedForOption } = usePoll();
|
const { poll, getParticipantsWhoVotedForOption } = usePoll();
|
||||||
const { mutate: updatePollMutation, isLoading: isUpdating } =
|
const { mutate: updatePollMutation, isLoading: isUpdating } =
|
||||||
useUpdatePollMutation();
|
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;
|
export default Page;
|
|
@ -1,22 +1,20 @@
|
||||||
|
"use client";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { CardFooter } from "@rallly/ui/card";
|
import { CardFooter } from "@rallly/ui/card";
|
||||||
import { Form } from "@rallly/ui/form";
|
import { Form } from "@rallly/ui/form";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PollSettingsForm,
|
PollSettingsForm,
|
||||||
PollSettingsFormData,
|
PollSettingsFormData,
|
||||||
} from "@/components/forms/poll-settings";
|
} from "@/components/forms/poll-settings";
|
||||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
|
||||||
import { useUpdatePollMutation } from "@/components/poll/mutations";
|
import { useUpdatePollMutation } from "@/components/poll/mutations";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { usePoll } from "@/contexts/poll";
|
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 poll = usePoll();
|
||||||
|
|
||||||
const router = useRouter();
|
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;
|
export default Page;
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
CardContent,
|
CardContent,
|
||||||
|
@ -6,10 +7,9 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@rallly/ui/card";
|
} from "@rallly/ui/card";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { Card } from "@/components/card";
|
import { Card } from "@/components/card";
|
||||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
|
||||||
import { PayWall } from "@/components/pay-wall";
|
import { PayWall } from "@/components/pay-wall";
|
||||||
import { FinalizePollForm } from "@/components/poll/manage-poll/finalize-poll-dialog";
|
import { FinalizePollForm } from "@/components/poll/manage-poll/finalize-poll-dialog";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
@ -18,7 +18,6 @@ import { usePoll } from "@/contexts/poll";
|
||||||
import { NextPageWithLayout } from "@/types";
|
import { NextPageWithLayout } from "@/types";
|
||||||
import { usePostHog } from "@/utils/posthog";
|
import { usePostHog } from "@/utils/posthog";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
|
||||||
|
|
||||||
const FinalizationForm = () => {
|
const FinalizationForm = () => {
|
||||||
const plan = usePlan();
|
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;
|
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 { cn } from "@rallly/ui";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
|
|
||||||
import { getPollLayout } from "@/components/layouts/poll-layout";
|
|
||||||
import { LoginLink } from "@/components/login-link";
|
import { LoginLink } from "@/components/login-link";
|
||||||
import { Poll } from "@/components/poll";
|
import { Poll } from "@/components/poll";
|
||||||
import { RegisterLink } from "@/components/register-link";
|
import { RegisterLink } from "@/components/register-link";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { usePoll } from "@/contexts/poll";
|
||||||
import { NextPageWithLayout } from "@/types";
|
|
||||||
import { isSelfHosted } from "@/utils/constants";
|
|
||||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
|
||||||
|
|
||||||
const GuestPollAlert = () => {
|
const GuestPollAlert = () => {
|
||||||
const poll = usePoll();
|
const poll = usePoll();
|
||||||
|
@ -49,25 +46,11 @@ const GuestPollAlert = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className={cn("mx-auto w-full max-w-4xl space-y-3 sm:space-y-4")}>
|
<div className={cn("mx-auto w-full max-w-4xl space-y-3 sm:space-y-4")}>
|
||||||
<GuestPollAlert />
|
<GuestPollAlert />
|
||||||
<Poll />
|
<Poll />
|
||||||
</div>
|
</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 {
|
import {
|
||||||
InboxIcon,
|
InboxIcon,
|
||||||
PauseCircleIcon,
|
PauseCircleIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RadioIcon,
|
RadioIcon,
|
||||||
VoteIcon,
|
VoteIcon,
|
||||||
} from "@rallly/icons";
|
} from "lucide-react";
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
|
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { DateIcon } from "@/components/date-icon";
|
import { DateIcon } from "@/components/date-icon";
|
||||||
import { getStandardLayout } from "@/components/layouts/standard-layout";
|
|
||||||
import {
|
import {
|
||||||
TopBar,
|
TopBar,
|
||||||
TopBarTitle,
|
TopBarTitle,
|
||||||
|
@ -22,11 +20,8 @@ import { ParticipantAvatarBar } from "@/components/participant-avatar-bar";
|
||||||
import { PollStatusBadge } from "@/components/poll-status";
|
import { PollStatusBadge } from "@/components/poll-status";
|
||||||
import { Skeleton } from "@/components/skeleton";
|
import { Skeleton } from "@/components/skeleton";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { NextPageWithLayout } from "@/types";
|
|
||||||
import { isSelfHosted } from "@/utils/constants";
|
|
||||||
import { useDayjs } from "@/utils/dayjs";
|
import { useDayjs } from "@/utils/dayjs";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
|
||||||
|
|
||||||
const EmptyState = () => {
|
const EmptyState = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -57,16 +52,12 @@ const EmptyState = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
export function PollsPage() {
|
||||||
const { data } = trpc.polls.list.useQuery();
|
const { data } = trpc.polls.list.useQuery();
|
||||||
const { t } = useTranslation();
|
|
||||||
const { adjustTimeZone } = useDayjs();
|
const { adjustTimeZone } = useDayjs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Head>
|
|
||||||
<title>{t("polls")}</title>
|
|
||||||
</Head>
|
|
||||||
<TopBar className="flex items-center justify-between gap-4">
|
<TopBar className="flex items-center justify-between gap-4">
|
||||||
<TopBarTitle title={<Trans i18nKey="polls" />} icon={VoteIcon} />
|
<TopBarTitle title={<Trans i18nKey="polls" />} icon={VoteIcon} />
|
||||||
<div>
|
<div>
|
||||||
|
@ -185,11 +176,4 @@ const Page: NextPageWithLayout = () => {
|
||||||
</div>
|
</div>
|
||||||
</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 { Button } from "@rallly/ui/button";
|
||||||
import { Card } from "@rallly/ui/card";
|
import { Card } from "@rallly/ui/card";
|
||||||
import { Label } from "@rallly/ui/label";
|
import { Label } from "@rallly/ui/label";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { GetStaticProps } from "next";
|
import { ArrowUpRight, CreditCardIcon, SendIcon } from "lucide-react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
import { BillingPlans } from "@/components/billing/billing-plans";
|
import { BillingPlans } from "@/components/billing/billing-plans";
|
||||||
import { getProfileLayout } from "@/components/layouts/profile-layout";
|
|
||||||
import {
|
import {
|
||||||
Settings,
|
Settings,
|
||||||
SettingsContent,
|
SettingsContent,
|
||||||
|
@ -19,12 +18,8 @@ import {
|
||||||
} from "@/components/settings/settings";
|
} from "@/components/settings/settings";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useSubscription } from "@/contexts/plan";
|
import { useSubscription } from "@/contexts/plan";
|
||||||
import { isSelfHosted } from "@/utils/constants";
|
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
|
|
||||||
import { NextPageWithLayout } from "../../types";
|
|
||||||
import { getStaticTranslations } from "../../utils/with-page-translations";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -61,11 +56,7 @@ const BillingPortal = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const proPlanIdMonthly = process.env
|
const proPlanIdMonthly = process.env.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string;
|
||||||
.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string;
|
|
||||||
|
|
||||||
export const proPlanIdYearly = process.env
|
|
||||||
.NEXT_PUBLIC_PRO_PLAN_ID_YEARLY as string;
|
|
||||||
|
|
||||||
const SubscriptionStatus = () => {
|
const SubscriptionStatus = () => {
|
||||||
const data = useSubscription();
|
const data = useSubscription();
|
||||||
|
@ -106,7 +97,7 @@ const LegacyBilling = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<>
|
||||||
{process.env.NEXT_PUBLIC_PADDLE_VENDOR_ID ? (
|
{process.env.NEXT_PUBLIC_PADDLE_VENDOR_ID ? (
|
||||||
<Script
|
<Script
|
||||||
src="https://cdn.paddle.com/paddle/paddle.js"
|
src="https://cdn.paddle.com/paddle/paddle.js"
|
||||||
|
@ -215,7 +206,104 @@ const LegacyBilling = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
</Card>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -241,7 +329,7 @@ const LegacyBilling = () => {
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>;
|
</SettingsSection>;
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
export function BillingPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -291,18 +379,4 @@ const Page: NextPageWithLayout = () => {
|
||||||
</SettingsContent>
|
</SettingsContent>
|
||||||
</Settings>
|
</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 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 { DateTimePreferences } from "@/components/settings/date-time-preferences";
|
||||||
import { LanguagePreference } from "@/components/settings/language-preference";
|
import { LanguagePreference } from "@/components/settings/language-preference";
|
||||||
import {
|
import {
|
||||||
|
@ -12,10 +12,7 @@ import {
|
||||||
} from "@/components/settings/settings";
|
} from "@/components/settings/settings";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
|
||||||
import { NextPageWithLayout } from "../../types";
|
export function PreferencesPage() {
|
||||||
import { getStaticTranslations } from "../../utils/with-page-translations";
|
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -52,10 +49,4 @@ const Page: NextPageWithLayout = () => {
|
||||||
</SettingsContent>
|
</SettingsContent>
|
||||||
</Settings>
|
</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 Head from "next/head";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
import { getProfileLayout } from "@/components/layouts/profile-layout";
|
|
||||||
import { ProfileSettings } from "@/components/settings/profile-settings";
|
import { ProfileSettings } from "@/components/settings/profile-settings";
|
||||||
import {
|
import {
|
||||||
Settings,
|
Settings,
|
||||||
|
@ -11,10 +11,7 @@ import {
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
|
|
||||||
import { NextPageWithLayout } from "../../types";
|
export const ProfilePage = () => {
|
||||||
import { getStaticTranslations } from "../../utils/with-page-translations";
|
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
|
@ -69,10 +66,3 @@ const Page: NextPageWithLayout = () => {
|
||||||
</Settings>
|
</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 { Button } from "@rallly/ui/button";
|
||||||
import Head from "next/head";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import { usePostHog } from "posthog-js/react";
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
@ -9,16 +9,11 @@ import React from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useDefaultEmail, VerifyCode } from "@/components/auth/auth-forms";
|
import { useDefaultEmail, VerifyCode } from "@/components/auth/auth-forms";
|
||||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
|
||||||
import { TextInput } from "@/components/text-input";
|
import { TextInput } from "@/components/text-input";
|
||||||
import { NextPageWithLayout } from "@/types";
|
|
||||||
import { useDayjs } from "@/utils/dayjs";
|
import { useDayjs } from "@/utils/dayjs";
|
||||||
import { requiredString, validEmail } from "@/utils/form-validation";
|
import { requiredString, validEmail } from "@/utils/form-validation";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
|
|
||||||
import { AuthLayout } from "../components/auth/auth-layout";
|
|
||||||
import { getStaticTranslations } from "../utils/with-page-translations";
|
|
||||||
|
|
||||||
type RegisterFormData = {
|
type RegisterFormData = {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -30,7 +25,8 @@ export const RegisterForm: React.FunctionComponent<{
|
||||||
const [defaultEmail, setDefaultEmail] = useDefaultEmail();
|
const [defaultEmail, setDefaultEmail] = useDefaultEmail();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { timeZone } = useDayjs();
|
const { timeZone } = useDayjs();
|
||||||
const router = useRouter();
|
const params = useParams<{ locale: string }>();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const { register, handleSubmit, getValues, setError, formState } =
|
const { register, handleSubmit, getValues, setError, formState } =
|
||||||
useForm<RegisterFormData>({
|
useForm<RegisterFormData>({
|
||||||
defaultValues: { email: defaultEmail },
|
defaultValues: { email: defaultEmail },
|
||||||
|
@ -47,7 +43,7 @@ export const RegisterForm: React.FunctionComponent<{
|
||||||
<VerifyCode
|
<VerifyCode
|
||||||
onSubmit={async (code) => {
|
onSubmit={async (code) => {
|
||||||
// get user's time zone
|
// get user's time zone
|
||||||
const locale = router.locale;
|
const locale = params?.locale ?? "en";
|
||||||
const res = await authenticateRegistration.mutateAsync({
|
const res = await authenticateRegistration.mutateAsync({
|
||||||
token,
|
token,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
@ -72,7 +68,7 @@ export const RegisterForm: React.FunctionComponent<{
|
||||||
|
|
||||||
signIn("registration-token", {
|
signIn("registration-token", {
|
||||||
token,
|
token,
|
||||||
callbackUrl: router.query.callbackUrl as string,
|
callbackUrl: searchParams?.get("callbackUrl") ?? undefined,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onChange={() => setToken(undefined)}
|
onChange={() => setToken(undefined)}
|
||||||
|
@ -181,24 +177,3 @@ export const RegisterForm: React.FunctionComponent<{
|
||||||
</form>
|
</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 { Button } from "@rallly/ui/button";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { GetServerSideProps } from "next";
|
import { useRouter } from "next/navigation";
|
||||||
import Head from "next/head";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useSession } from "next-auth/react";
|
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 { Logo } from "@/components/logo";
|
||||||
import { Skeleton } from "@/components/skeleton";
|
import { Skeleton } from "@/components/skeleton";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { UserAvatar } from "@/components/user";
|
import { UserAvatar } from "@/components/user";
|
||||||
import { NextPageWithLayout } from "@/types";
|
|
||||||
import { usePostHog } from "@/utils/posthog";
|
import { usePostHog } from "@/utils/posthog";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
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 };
|
type PageProps = { magicLink: string; email: string };
|
||||||
|
|
||||||
const Page: NextPageWithLayout<PageProps> = ({ magicLink, email }) => {
|
export const LoginPage = ({ magicLink, email }: PageProps) => {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
const trpcUtils = trpc.useUtils();
|
const trpcUtils = trpc.useUtils();
|
||||||
|
@ -61,12 +46,8 @@ const Page: NextPageWithLayout<PageProps> = ({ magicLink, email }) => {
|
||||||
});
|
});
|
||||||
const { data } = trpc.user.getByEmail.useQuery({ email });
|
const { data } = trpc.user.getByEmail.useQuery({ email });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col items-center justify-center gap-4 p-4">
|
<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">
|
<div className="mb-6">
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,43 +86,3 @@ const Page: NextPageWithLayout<PageProps> = ({ magicLink, email }) => {
|
||||||
</div>
|
</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 { Button } from "@rallly/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from "next-auth/react";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
@ -133,7 +134,7 @@ export const LoginForm: React.FunctionComponent<{
|
||||||
const [email, setEmail] = React.useState<string>();
|
const [email, setEmail] = React.useState<string>();
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const callbackUrl = (router.query.callbackUrl as string) ?? "/";
|
const callbackUrl = (useSearchParams()?.get("callbackUrl") as string) ?? "/";
|
||||||
|
|
||||||
const sendVerificationEmail = (email: string) => {
|
const sendVerificationEmail = (email: string) => {
|
||||||
return signIn("email", {
|
return signIn("email", {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { TrendingUpIcon } from "@rallly/icons";
|
|
||||||
import {
|
import {
|
||||||
BillingPlan,
|
BillingPlan,
|
||||||
BillingPlanDescription,
|
BillingPlanDescription,
|
||||||
|
@ -11,6 +10,7 @@ import {
|
||||||
} from "@rallly/ui/billing-plan";
|
} from "@rallly/ui/billing-plan";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
||||||
|
import { TrendingUpIcon } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { GlobeIcon } from "@rallly/icons";
|
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
@ -10,6 +9,7 @@ import {
|
||||||
} from "@rallly/ui/dialog";
|
} from "@rallly/ui/dialog";
|
||||||
import { Label } from "@rallly/ui/label";
|
import { Label } from "@rallly/ui/label";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { GlobeIcon } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useInterval } from "react-use";
|
import { useInterval } from "react-use";
|
||||||
import spacetime from "spacetime";
|
import spacetime from "spacetime";
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
@ -7,7 +8,7 @@ import {
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@rallly/ui/card";
|
} from "@rallly/ui/card";
|
||||||
import { Form } from "@rallly/ui/form";
|
import { Form } from "@rallly/ui/form";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import useFormPersist from "react-hook-form-persist";
|
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 { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
@ -12,6 +7,7 @@ import {
|
||||||
} from "@rallly/ui/dropdown-menu";
|
} from "@rallly/ui/dropdown-menu";
|
||||||
import { Textarea } from "@rallly/ui/textarea";
|
import { Textarea } from "@rallly/ui/textarea";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { MessageCircleIcon, MoreHorizontalIcon, TrashIcon } from "lucide-react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { FrownIcon } from "@rallly/icons";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import Head from "next/head";
|
import { FrownIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
@ -19,9 +18,6 @@ const ErrorPage: React.FunctionComponent<ComponentProps> = ({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-100px)] w-full items-center justify-center">
|
<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-8">
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<Icon className="mb-4 inline-block h-24 w-24 text-gray-400" />
|
<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 dayjs from "dayjs";
|
||||||
|
import { MapPinIcon, MousePointerClickIcon, TextIcon } from "lucide-react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
import { Card } from "@/components/card";
|
import { Card } from "@/components/card";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { HelpCircleIcon } from "@rallly/icons";
|
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||||
|
import { HelpCircleIcon } from "lucide-react";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
import {
|
|
||||||
BugIcon,
|
|
||||||
LifeBuoyIcon,
|
|
||||||
LightbulbIcon,
|
|
||||||
MegaphoneIcon,
|
|
||||||
SmileIcon,
|
|
||||||
} from "@rallly/icons";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
@ -13,6 +6,13 @@ import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@rallly/ui/dropdown-menu";
|
} from "@rallly/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
BugIcon,
|
||||||
|
LifeBuoyIcon,
|
||||||
|
LightbulbIcon,
|
||||||
|
MegaphoneIcon,
|
||||||
|
SmileIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "@rallly/icons";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
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 { cn } from "@rallly/ui";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
|
@ -20,6 +11,15 @@ import {
|
||||||
import { Switch } from "@rallly/ui/switch";
|
import { Switch } from "@rallly/ui/switch";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
PlusIcon,
|
||||||
|
SparklesIcon,
|
||||||
|
XIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,9 @@ import {
|
||||||
useFloating,
|
useFloating,
|
||||||
} from "@floating-ui/react-dom-interactions";
|
} from "@floating-ui/react-dom-interactions";
|
||||||
import { Listbox } from "@headlessui/react";
|
import { Listbox } from "@headlessui/react";
|
||||||
import { ChevronDownIcon } from "@rallly/icons";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { getDuration } from "@/utils/date-time-utils";
|
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 { Card, CardDescription, CardHeader, CardTitle } from "@rallly/ui/card";
|
||||||
import { FormField, FormMessage } from "@rallly/ui/form";
|
import { FormField, FormMessage } from "@rallly/ui/form";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
||||||
|
import { CalendarIcon, TableIcon } from "lucide-react";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { XIcon } from "@rallly/icons";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { XIcon } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Calendar } from "react-big-calendar";
|
import { Calendar } from "react-big-calendar";
|
||||||
import { createBreakpoint } from "react-use";
|
import { createBreakpoint } from "react-use";
|
||||||
|
@ -98,7 +98,7 @@ const WeekCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
||||||
width: `calc(${props.style?.width}%)`,
|
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" />
|
<XIcon className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
import {
|
|
||||||
AtSignIcon,
|
|
||||||
EyeIcon,
|
|
||||||
InfoIcon,
|
|
||||||
MessageCircleIcon,
|
|
||||||
VoteIcon,
|
|
||||||
} from "@rallly/icons";
|
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
@ -16,6 +9,13 @@ import {
|
||||||
import { FormField } from "@rallly/ui/form";
|
import { FormField } from "@rallly/ui/form";
|
||||||
import { Switch } from "@rallly/ui/switch";
|
import { Switch } from "@rallly/ui/switch";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||||
|
import {
|
||||||
|
AtSignIcon,
|
||||||
|
EyeIcon,
|
||||||
|
InfoIcon,
|
||||||
|
MessageCircleIcon,
|
||||||
|
VoteIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { ArrowUpRightIcon, Share2Icon } from "@rallly/icons";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
@ -8,6 +7,7 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@rallly/ui/dialog";
|
} from "@rallly/ui/dialog";
|
||||||
|
import { ArrowUpRightIcon, Share2Icon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useCopyToClipboard } from "react-use";
|
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 {
|
import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
|
@ -9,18 +18,10 @@ import {
|
||||||
PlayCircleIcon,
|
PlayCircleIcon,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
ShieldCloseIcon,
|
ShieldCloseIcon,
|
||||||
} from "@rallly/icons";
|
} from "lucide-react";
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuItemIconLabel,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@rallly/ui/dropdown-menu";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
|
@ -46,7 +47,6 @@ import { Skeleton } from "@/components/skeleton";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { usePoll } from "@/contexts/poll";
|
||||||
import Error404 from "@/pages/404";
|
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
|
|
||||||
import { NextPageWithLayout } from "../../types";
|
import { NextPageWithLayout } from "../../types";
|
||||||
|
@ -155,12 +155,12 @@ const StatusControl = () => {
|
||||||
const AdminControls = () => {
|
const AdminControls = () => {
|
||||||
const poll = usePoll();
|
const poll = usePoll();
|
||||||
const pollLink = `/poll/${poll.id}`;
|
const pollLink = `/poll/${poll.id}`;
|
||||||
const router = useRouter();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<TopBar>
|
<TopBar>
|
||||||
<div className="flex flex-col items-start justify-between gap-x-4 gap-y-2 sm:flex-row">
|
<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">
|
<div className="flex min-w-0 gap-4">
|
||||||
{router.asPath !== pollLink ? (
|
{pathname !== pollLink ? (
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={pollLink}>
|
<Link href={pollLink}>
|
||||||
<ArrowLeftIcon className="h-4 w-4" />
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
@ -257,18 +257,14 @@ const Title = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Prefetch = ({ children }: React.PropsWithChildren) => {
|
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 poll = trpc.polls.get.useQuery({ urlId });
|
||||||
const participants = trpc.polls.participants.list.useQuery({ pollId: urlId });
|
const participants = trpc.polls.participants.list.useQuery({ pollId: urlId });
|
||||||
const watchers = trpc.polls.getWatchers.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) {
|
if (!poll.data || !watchers.data || !participants.data) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -287,10 +283,10 @@ const Prefetch = ({ children }: React.PropsWithChildren) => {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PollLayout = ({ children }: React.PropsWithChildren) => {
|
export const PollLayout = ({ children }: React.PropsWithChildren) => {
|
||||||
const router = useRouter();
|
const params = useParams();
|
||||||
|
|
||||||
const urlId = router.query.urlId as string;
|
const urlId = params?.urlId as string;
|
||||||
|
|
||||||
if (!urlId) {
|
if (!urlId) {
|
||||||
// probably navigating away
|
// 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 {
|
import {
|
||||||
CreditCardIcon,
|
CreditCardIcon,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
Settings2Icon,
|
Settings2Icon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
} from "@rallly/icons";
|
} from "lucide-react";
|
||||||
import { cn } from "@rallly/ui";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import { Card } from "@rallly/ui/card";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { usePathname } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
import { useToggle } from "react-use";
|
import { useToggle } from "react-use";
|
||||||
|
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
|
||||||
import { IfCloudHosted } from "@/contexts/environment";
|
import { IfCloudHosted } from "@/contexts/environment";
|
||||||
import { Plan } from "@/contexts/plan";
|
import { Plan } from "@/contexts/plan";
|
||||||
|
|
||||||
import { IconComponent, NextPageWithLayout } from "../../types";
|
import { IconComponent } from "../../types";
|
||||||
import { IfAuthenticated, useUser } from "../user-provider";
|
import { IfAuthenticated, useUser } from "../user-provider";
|
||||||
|
|
||||||
const MenuItem = (props: {
|
const MenuItem = (props: {
|
||||||
|
@ -27,12 +26,12 @@ const MenuItem = (props: {
|
||||||
href: string;
|
href: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex min-w-0 items-center gap-x-2.5 px-2.5 py-1.5 text-sm font-medium",
|
"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"
|
? "bg-gray-200"
|
||||||
: "text-gray-500 hover:bg-gray-100 hover:text-gray-800",
|
: "text-gray-500 hover:bg-gray-100 hover:text-gray-800",
|
||||||
)}
|
)}
|
||||||
|
@ -48,13 +47,13 @@ export const ProfileLayout = ({ children }: React.PropsWithChildren) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
// reset toggle whenever route changes
|
// reset toggle whenever route changes
|
||||||
const router = useRouter();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [isMenuOpen, toggle] = useToggle(false);
|
const [isMenuOpen, toggle] = useToggle(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
toggle(false);
|
toggle(false);
|
||||||
}, [router.asPath, toggle]);
|
}, [pathname, toggle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -101,12 +100,3 @@ export const ProfileLayout = ({ children }: React.PropsWithChildren) => {
|
||||||
</div>
|
</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 { cn } from "@rallly/ui";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { AnimatePresence, m } from "framer-motion";
|
import { AnimatePresence, m } from "framer-motion";
|
||||||
|
import { ListIcon, SparklesIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { usePathname } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
|
@ -17,7 +18,6 @@ import {
|
||||||
import FeedbackButton from "@/components/feedback";
|
import FeedbackButton from "@/components/feedback";
|
||||||
import { LoginLink } from "@/components/login-link";
|
import { LoginLink } from "@/components/login-link";
|
||||||
import { Logo } from "@/components/logo";
|
import { Logo } from "@/components/logo";
|
||||||
import { Spinner } from "@/components/spinner";
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { UserDropdown } from "@/components/user-dropdown";
|
import { UserDropdown } from "@/components/user-dropdown";
|
||||||
import { IfCloudHosted } from "@/contexts/environment";
|
import { IfCloudHosted } from "@/contexts/environment";
|
||||||
|
@ -41,14 +41,14 @@ const NavMenuItem = ({
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<Button variant="ghost" asChild>
|
<Button variant="ghost" asChild>
|
||||||
<Link
|
<Link
|
||||||
target={target}
|
target={target}
|
||||||
href={href}
|
href={href}
|
||||||
className={cn(
|
className={cn(
|
||||||
router.asPath === href
|
pathname === href
|
||||||
? "text-foreground"
|
? "text-foreground"
|
||||||
: "text-muted-foreground hover:text-foreground active:bg-gray-200/50",
|
: "text-muted-foreground hover:text-foreground active:bg-gray-200/50",
|
||||||
className,
|
className,
|
||||||
|
@ -78,18 +78,6 @@ const Upgrade = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const LogoArea = () => {
|
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 (
|
return (
|
||||||
<div className="relative flex items-center justify-center gap-x-4">
|
<div className="relative flex items-center justify-center gap-x-4">
|
||||||
<Link
|
<Link
|
||||||
|
@ -100,14 +88,6 @@ const LogoArea = () => {
|
||||||
>
|
>
|
||||||
<Logo size="sm" />
|
<Logo size="sm" />
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Link, { LinkProps } from "next/link";
|
import Link, { LinkProps } from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const LoginLink = React.forwardRef<
|
export const LoginLink = React.forwardRef<
|
||||||
|
@ -7,6 +7,7 @@ export const LoginLink = React.forwardRef<
|
||||||
React.PropsWithChildren<Omit<LinkProps, "href"> & { className?: string }>
|
React.PropsWithChildren<Omit<LinkProps, "href"> & { className?: string }>
|
||||||
>(function LoginLink({ children, ...props }, ref) {
|
>(function LoginLink({ children, ...props }, ref) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname() ?? "/";
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -15,7 +16,7 @@ export const LoginLink = React.forwardRef<
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
props.onClick?.(e);
|
props.onClick?.(e);
|
||||||
router.push("/login?callbackUrl=" + encodeURIComponent(router.asPath));
|
router.push("/login?callbackUrl=" + encodeURIComponent(pathname));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Dialog } from "@headlessui/react";
|
import { Dialog } from "@headlessui/react";
|
||||||
import { XIcon } from "@rallly/icons";
|
|
||||||
import { AnimatePresence, m } from "framer-motion";
|
import { AnimatePresence, m } from "framer-motion";
|
||||||
|
import { XIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { ButtonProps, LegacyButton } from "../button";
|
import { ButtonProps, LegacyButton } from "../button";
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { PencilIcon, TagIcon, TrashIcon } from "@rallly/icons";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
@ -27,6 +26,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@rallly/ui/form";
|
} from "@rallly/ui/form";
|
||||||
import { Input } from "@rallly/ui/input";
|
import { Input } from "@rallly/ui/input";
|
||||||
|
import { PencilIcon, TagIcon, TrashIcon } from "lucide-react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
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 {
|
import {
|
||||||
CalendarCheck2Icon,
|
CalendarCheck2Icon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
|
@ -7,14 +12,9 @@ import {
|
||||||
LockIcon,
|
LockIcon,
|
||||||
Settings2Icon,
|
Settings2Icon,
|
||||||
TrendingUpIcon,
|
TrendingUpIcon,
|
||||||
} from "@rallly/icons";
|
} from "lucide-react";
|
||||||
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 Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useParams } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
@ -51,7 +51,7 @@ const Feature = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const Teaser = () => {
|
const Teaser = () => {
|
||||||
const router = useRouter();
|
const params = useParams();
|
||||||
|
|
||||||
const [tab, setTab] = React.useState("yearly");
|
const [tab, setTab] = React.useState("yearly");
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ const Teaser = () => {
|
||||||
<Trans i18nKey="upgrade" defaults="Upgrade" />
|
<Trans i18nKey="upgrade" defaults="Upgrade" />
|
||||||
</UpgradeButton>
|
</UpgradeButton>
|
||||||
<Button asChild className="w-full">
|
<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" />
|
<Trans i18nKey="notToday" defaults="Not Today" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Participant, VoteType } from "@rallly/database";
|
import { Participant, VoteType } from "@rallly/database";
|
||||||
import { TrashIcon } from "@rallly/icons";
|
|
||||||
import { keyBy } from "lodash";
|
import { keyBy } from "lodash";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CheckCircleIcon, PauseCircleIcon, RadioIcon } from "@rallly/icons";
|
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
|
import { CheckCircleIcon, PauseCircleIcon, RadioIcon } from "lucide-react";
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { IconComponent } from "@/types";
|
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 {
|
import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
|
@ -5,10 +8,7 @@ import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
ShrinkIcon,
|
ShrinkIcon,
|
||||||
Users2Icon,
|
Users2Icon,
|
||||||
} from "@rallly/icons";
|
} from "lucide-react";
|
||||||
import { cn } from "@rallly/ui";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { RemoveScroll } from "react-remove-scroll";
|
import { RemoveScroll } from "react-remove-scroll";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Participant, VoteType } from "@rallly/database";
|
import { Participant, VoteType } from "@rallly/database";
|
||||||
import { MoreHorizontalIcon } from "@rallly/icons";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { MoreHorizontalIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { ParticipantDropdown } from "@/components/participant-dropdown";
|
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