♻️ Switch to app router (#922)

This commit is contained in:
Luke Vella 2023-11-06 09:15:49 +00:00 committed by GitHub
parent 41f85279bb
commit 95feb9f01a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
181 changed files with 2507 additions and 2494 deletions

11
.env.development Normal file
View 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

View file

@ -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"
}
}

View file

@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
steps:
# Downloads a copy of the code in your repository before running CI tests
- name: Check out repository code
@ -56,21 +56,21 @@ jobs:
node-version: 18
cache: yarn
- name: Set environment variables
run: |
echo "DATABASE_URL=postgresql://postgres:password@localhost:5432/rallly" >> $GITHUB_ENV
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Set environment variables
run: |
echo "DATABASE_URL=postgresql://postgres:password@localhost:5450/db" >> $GITHUB_ENV
- name: Run db
run: |
docker pull postgres:14.2
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_DB=rallly postgres:14.2
yarn wait-on --timeout 60000 tcp:localhost:5432
docker run -d -p 5450:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_DB=rallly postgres:14.2
yarn wait-on --timeout 60000 tcp:localhost:5450
- name: Deploy migrations
run: yarn db:deploy
run: yarn db:setup
- name: Install playwright dependencies
run: yarn playwright install --with-deps chromium

View file

@ -24,45 +24,53 @@ Built with [Next.js](https://github.com/vercel/next.js/), [Prisma](https://githu
Check out the [self-hosting docs](https://support.rallly.co/self-hosting) for more information on running your own instance of Rallly.
## Development
## Get started
Clone this repo and change directory to the root of the repository.
1. Clone the repository switch to the project directory
```bash
git clone https://github.com/lukevella/rallly.git
cd rallly
```
```bash
git clone https://github.com/lukevella/rallly.git
cd rallly
```
Install dependencies
2. Install dependencies
```
yarn
```
```
yarn
```
Copy the sample `.env` file then open it and set the required [configuration options](https://support.rallly.co/self-hosting/configuration-options).
3. Setup environment variables
```bash
cp sample.env .env
```
```bash
cp sample.env .env
```
Next, run the following command:
Create a `.env` file by copying `sample.env` then open it and set the required [configuration options](https://support.rallly.co/self-hosting/configuration-options).
```
yarn db:generate && yarn db:reset
```
4. Setup the database
This will:
If you don't have a postgres database running locally, you can spin up a new database using docker by running:
- generate the prisma database client
- run migrations to create the database schema
- seed the database with some random data
```
yarn dx
```
Start the Next.js server
If you already have a postgres database, you can run the migrations and seed the database by running:
```
# For development
yarn dev
```
```
yarn db:setup
```
This will:
- run migrations to create the database schema
- seed the database with test users and random data
5. Start the Next.js server
```
yarn dev
```
## Contributors

4
apps/docs/.eslintrc.js Normal file
View file

@ -0,0 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
...require("@rallly/eslint-config")(__dirname),
};

View file

@ -1,5 +1,8 @@
{
"name": "@rallly/docs",
"version": "0.0.0",
"private": true
"private": true,
"devDependencies": {
"@rallly/tsconfig": "*"
}
}

5
apps/docs/tsconfig.json Normal file
View file

@ -0,0 +1,5 @@
{
"extends": "@rallly/tsconfig/next.json",
"include": ["**/*.ts", "**/*.tsx", "**/*.js"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
...require("@rallly/eslint-config")(__dirname),
};

View file

@ -20,7 +20,6 @@
"@svgr/webpack": "^6.5.1",
"@tailwindcss/typography": "^0.5.9",
"@vercel/analytics": "^0.1.8",
"@vercel/og": "^0.5.11",
"autoprefixer": "^10.4.13",
"class-variance-authority": "^0.6.0",
"dayjs": "^1.11.7",
@ -50,15 +49,8 @@
"@types/smoothscroll-polyfill": "^0.3.1",
"cheerio": "^1.0.0-rc.12",
"cross-env": "^7.0.3",
"eslint-config-turbo": "^0.0.9",
"eslint-import-resolver-typescript": "^2.7.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"i18next-scanner": "^4.2.0",
"i18next-scanner-typescript": "^1.1.1",
"prettier-plugin-tailwindcss": "^0.1.8",
"smtp-tester": "^2.0.1",
"wait-on": "^6.0.1"
}

View file

@ -1,4 +1,4 @@
import { FrownIcon } from "@rallly/icons";
import { FrownIcon } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useTranslation } from "next-i18next";

View file

@ -1,11 +1,11 @@
import { cn } from "@rallly/ui";
import { m } from "framer-motion";
import {
CalendarCheck2Icon,
LanguagesIcon,
Users2Icon,
ZapIcon,
} from "@rallly/icons";
import { cn } from "@rallly/ui";
import { m } from "framer-motion";
} from "lucide-react";
import { Trans } from "@/components/trans";
import { IconComponent } from "@/types";

View file

@ -40,7 +40,7 @@ function isBright(color: RGBColor): boolean {
export const getRandomAvatarColor = (str: string) => {
const strSum = str.split("").reduce((acc, val) => acc + val.charCodeAt(0), 0);
const randomIndex = strSum % avatarBackgroundColors.length;
const color = avatarBackgroundColors[randomIndex];
const color = avatarBackgroundColors[randomIndex] as RGBColor;
const [r, g, b] = color;
return { color: `rgb(${r}, ${g}, ${b})`, requiresDarkText: isBright(color) };
};

View file

@ -1,8 +1,8 @@
import { ChevronRightIcon } from "@rallly/icons";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button";
import { preventWidows } from "@rallly/utils";
import { m } from "framer-motion";
import { ChevronRightIcon } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import * as React from "react";

View file

@ -1,4 +1,4 @@
import { NewspaperIcon } from "@rallly/icons";
import { NewspaperIcon } from "lucide-react";
import Script from "next/script";
import PageLayout from "@/components/layouts/page-layout";

View file

@ -1,4 +1,3 @@
import { ChevronRightIcon, MenuIcon } from "@rallly/icons";
import { cn } from "@rallly/ui";
import {
DropdownMenu,
@ -7,6 +6,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@rallly/ui/dropdown-menu";
import { ChevronRightIcon, MenuIcon } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";

View file

@ -1,10 +1,4 @@
import {
DiscordIcon,
GithubIcon,
LanguagesIcon,
LinkedinIcon,
TwitterIcon,
} from "@rallly/icons";
import { DiscordIcon } from "@rallly/icons";
import languages from "@rallly/languages";
import {
Select,
@ -13,6 +7,12 @@ import {
SelectTrigger,
SelectValue,
} from "@rallly/ui/select";
import {
GithubIcon,
LanguagesIcon,
LinkedinIcon,
TwitterIcon,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";

View file

@ -1,5 +1,5 @@
import { ArrowUpRight } from "@rallly/icons";
import { m } from "framer-motion";
import { ArrowUpRight } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { NextSeo } from "next-seo";

View file

@ -1,4 +1,4 @@
import { FileSearchIcon } from "@rallly/icons";
import { FileSearchIcon } from "lucide-react";
import { GetStaticProps } from "next";
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

View file

@ -1,5 +1,5 @@
/* eslint-disable @next/next/no-img-element */
import { ImageResponse } from "@vercel/og";
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export const config = {

View file

@ -1,4 +1,4 @@
import { ArrowLeftIcon } from "@rallly/icons";
import { ArrowLeftIcon } from "lucide-react";
import { GetStaticPropsContext } from "next";
import ErrorPage from "next/error";
import Head from "next/head";
@ -117,6 +117,8 @@ export async function getStaticProps(ctx: GetStaticPropsContext) {
},
};
}
return res;
}
export async function getStaticPaths() {

View file

@ -1,4 +1,3 @@
import { TrendingUpIcon } from "@rallly/icons";
import {
BillingPlan,
BillingPlanDescription,
@ -11,6 +10,7 @@ import {
} from "@rallly/ui/billing-plan";
import { Button } from "@rallly/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
import { TrendingUpIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { NextSeo } from "next-seo";

View file

@ -1,24 +1,13 @@
{
"extends": "@rallly/tsconfig/next.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["public/*"]
},
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
"checkJs": false
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next-i18next.config.js"],
"exclude": ["node_modules", "**/*.js"]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -2,5 +2,5 @@ PORT=3002
NEXT_PUBLIC_BASE_URL=http://localhost:3002
NEXTAUTH_URL=http://localhost:3002
SECRET_PASSWORD=abcdefghijklmnopqrstuvwxyz1234567890
DATABASE_URL=postgres://postgres:postgres@localhost:5432/rallly
DATABASE_URL=postgres://postgres:postgres@localhost:5450/db
SUPPORT_EMAIL=support@rallly.co

4
apps/web/.eslintrc.js Normal file
View file

@ -0,0 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
...require("@rallly/eslint-config")(__dirname),
};

View file

@ -1,4 +1,4 @@
import "react-i18next";
import "i18next";
import app from "../public/locales/en/app.json";

View file

@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -4,17 +4,16 @@
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
const { withSentryConfig } = require("@sentry/nextjs");
const i18n = require("./i18n.config.js");
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
/** @type {import('next').NextConfig} */
const nextConfig = {
i18n: { ...i18n, localeDetection: false },
productionBrowserSourceMaps: true,
output: "standalone",
transpilePackages: [
"@rallly/backend",
"@rallly/database",
"@rallly/icons",
"@rallly/ui",
"@rallly/tailwind-config",
@ -22,12 +21,14 @@ const nextConfig = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
issuer: /\.[jt]sx?$/,
use: ["@svgr/webpack"],
});
return config;
},
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
@ -61,6 +62,7 @@ const sentryWebpackPluginOptions = {
// recommended:
// release, url, org, project, authToken, configFile, stripPrefix,
// urlPrefix, include, ignore
authToken: process.env.SENTRY_AUTH_TOKEN,
dryRun: !process.env.SENTRY_AUTH_TOKEN,
silent: true, // Suppresses all logs
// For all available options, see:

View file

@ -3,8 +3,9 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "cross-env TAILWIND_MODE=watch next dev",
"dev": "next dev",
"build": "next build",
"build:test": "dotenv -e .env.test -- next build",
"analyze": "cross-env ANALYZE=true next build",
"start": "next start",
"lint": "eslint .",
@ -29,7 +30,6 @@
"@rallly/languages": "*",
"@rallly/tailwind-config": "*",
"@rallly/ui": "*",
"@sentry/nextjs": "^7.74.1",
"@svgr/webpack": "^6.5.1",
"@tailwindcss/typography": "^0.5.9",
"@tanstack/react-query": "^4.0.0",
@ -37,7 +37,6 @@
"@trpc/client": "^10.13.0",
"@trpc/next": "^10.13.0",
"@trpc/react-query": "^10.13.0",
"@vercel/og": "^0.5.13",
"accept-language-parser": "^1.5.0",
"autoprefixer": "^10.4.13",
"class-variance-authority": "^0.6.0",
@ -48,6 +47,7 @@
"dayjs": "^1.11.10",
"i18next": "^22.4.9",
"i18next-icu": "^2.3.0",
"i18next-resources-to-backend": "^1.1.4",
"ics": "^3.1.0",
"intl-messageformat": "^10.3.4",
"iron-session": "^6.3.1",
@ -72,11 +72,12 @@
"smoothscroll-polyfill": "^0.4.4",
"spacetime": "^7.4.7",
"superjson": "^2.0.0",
"timezone-soft": "^1.4.1"
"timezone-soft": "^1.5.1"
},
"devDependencies": {
"@playwright/test": "^1.35.1",
"@playwright/test": "^1.39.0",
"@rallly/tsconfig": "*",
"@rallly/eslint-config": "*",
"@types/accept-language-parser": "^1.5.3",
"@types/color-hash": "^1.0.2",
"@types/lodash": "^4.14.178",
@ -87,15 +88,8 @@
"@types/smoothscroll-polyfill": "^0.3.1",
"cheerio": "^1.0.0-rc.12",
"cross-env": "^7.0.3",
"eslint-config-turbo": "^0.0.9",
"eslint-import-resolver-typescript": "^2.7.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"i18next-scanner": "^4.2.0",
"i18next-scanner-typescript": "^1.1.1",
"prettier-plugin-tailwindcss": "^0.1.8",
"smtp-tester": "^2.0.1",
"wait-on": "^6.0.1"
}

View file

@ -4,7 +4,7 @@ import path from "path";
const ci = process.env.CI === "true";
dotenv.config({ path: path.resolve(__dirname, "../../", ".env.test") });
dotenv.config({ path: path.resolve(__dirname, ".env.test") });
// Use process.env.PORT by default and fallback to port 3000
const PORT = process.env.PORT || 3000;
@ -24,13 +24,9 @@ const config: PlaywrightTestConfig = {
trace: "retain-on-failure",
},
webServer: {
command: `NODE_ENV=test yarn dev --port ${PORT}`,
command: `NODE_ENV=test yarn start --port ${PORT}`,
url: baseURL,
timeout: 120 * 1000,
reuseExistingServer: !ci,
},
expect: {
timeout: 10000, // 10 seconds
reuseExistingServer: !process.env.CI,
},
reporter: [
[ci ? "github" : "list"],

View file

@ -1,3 +1,6 @@
module.exports = {
plugins: ["tailwindcss", "autoprefixer"],
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View file

@ -225,5 +225,6 @@
"authErrorCta": "Go to login page",
"continueAs": "Continue as",
"finalizeFeature": "Finalize",
"duplicateFeature": "Duplicate"
"duplicateFeature": "Duplicate",
"pageMovedDescription": "Redirecting to <a>{newUrl}</a>"
}

View 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>;
}

View 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"),
};
}

View file

@ -1,3 +1,4 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@rallly/ui/button";
import {
@ -17,18 +18,16 @@ import {
} from "@rallly/ui/form";
import { Input } from "@rallly/ui/input";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { getPollLayout } from "@/components/layouts/poll-layout";
import { PayWall } from "@/components/pay-wall";
import { usePoll } from "@/components/poll-context";
import { Trans } from "@/components/trans";
import { NextPageWithLayout } from "@/types";
import { usePostHog } from "@/utils/posthog";
import { trpc } from "@/utils/trpc/client";
import { getStaticTranslations } from "@/utils/with-page-translations";
const formSchema = z.object({
title: z.string().trim().min(1),
@ -63,7 +62,7 @@ const Page: NextPageWithLayout = () => {
pollId: poll.id,
newPollId: res.id,
});
await router.push(`/poll/${res.id}`);
router.push(`/poll/${res.id}`);
},
},
);
@ -124,15 +123,4 @@ const Page: NextPageWithLayout = () => {
);
};
Page.getLayout = getPollLayout;
export const getStaticPaths = async () => {
return {
paths: [],
fallback: "blocking",
};
};
export const getStaticProps = getStaticTranslations;
export default Page;

View file

@ -1,3 +1,4 @@
"use client";
import { Button } from "@rallly/ui/button";
import {
Card,
@ -9,19 +10,17 @@ import {
} from "@rallly/ui/card";
import { Form } from "@rallly/ui/form";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import {
PollDetailsData,
PollDetailsForm,
} from "@/components/forms/poll-details-form";
import { getPollLayout } from "@/components/layouts/poll-layout";
import { useUpdatePollMutation } from "@/components/poll/mutations";
import { usePoll } from "@/components/poll-context";
import { Trans } from "@/components/trans";
import { NextPageWithLayout } from "@/types";
import { getStaticTranslations } from "@/utils/with-page-translations";
const Page: NextPageWithLayout = () => {
const { poll } = usePoll();
@ -87,15 +86,4 @@ const Page: NextPageWithLayout = () => {
);
};
Page.getLayout = getPollLayout;
export const getStaticPaths = async () => {
return {
paths: [],
fallback: "blocking",
};
};
export const getStaticProps = getStaticTranslations;
export default Page;

View file

@ -1,22 +1,20 @@
"use client";
import { Button } from "@rallly/ui/button";
import { CardFooter } from "@rallly/ui/card";
import { Form } from "@rallly/ui/form";
import dayjs from "dayjs";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useTranslation } from "next-i18next";
import { useForm } from "react-hook-form";
import { PollOptionsData } from "@/components/forms";
import PollOptionsForm from "@/components/forms/poll-options-form";
import { getPollLayout } from "@/components/layouts/poll-layout";
import { useModalContext } from "@/components/modal/modal-provider";
import { useUpdatePollMutation } from "@/components/poll/mutations";
import { usePoll } from "@/components/poll-context";
import { Trans } from "@/components/trans";
import { NextPageWithLayout } from "@/types";
import { encodeDateOption } from "@/utils/date-time-utils";
import { getStaticTranslations } from "@/utils/with-page-translations";
const convertOptionToString = (option: { start: Date; duration: number }) => {
const start = dayjs(option.start).utc();
@ -27,7 +25,7 @@ const convertOptionToString = (option: { start: Date; duration: number }) => {
.format("YYYY-MM-DDTHH:mm:ss")}`;
};
const Page: NextPageWithLayout = () => {
const Page = () => {
const { poll, getParticipantsWhoVotedForOption } = usePoll();
const { mutate: updatePollMutation, isLoading: isUpdating } =
useUpdatePollMutation();
@ -136,15 +134,4 @@ const Page: NextPageWithLayout = () => {
);
};
Page.getLayout = getPollLayout;
export const getStaticPaths = async () => {
return {
paths: [], //indicates that no page needs be created at build time
fallback: "blocking", //indicates the type of fallback
};
};
export const getStaticProps = getStaticTranslations;
export default Page;

View file

@ -1,22 +1,20 @@
"use client";
import { Button } from "@rallly/ui/button";
import { CardFooter } from "@rallly/ui/card";
import { Form } from "@rallly/ui/form";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import {
PollSettingsForm,
PollSettingsFormData,
} from "@/components/forms/poll-settings";
import { getPollLayout } from "@/components/layouts/poll-layout";
import { useUpdatePollMutation } from "@/components/poll/mutations";
import { Trans } from "@/components/trans";
import { usePoll } from "@/contexts/poll";
import { NextPageWithLayout } from "@/types";
import { getStaticTranslations } from "@/utils/with-page-translations";
const Page: NextPageWithLayout = () => {
const Page = () => {
const poll = usePoll();
const router = useRouter();
@ -69,15 +67,4 @@ const Page: NextPageWithLayout = () => {
);
};
Page.getLayout = getPollLayout;
export const getStaticPaths = async () => {
return {
paths: [],
fallback: "blocking",
};
};
export const getStaticProps = getStaticTranslations;
export default Page;

View file

@ -1,3 +1,4 @@
"use client";
import { Button } from "@rallly/ui/button";
import {
CardContent,
@ -6,10 +7,9 @@ import {
CardHeader,
CardTitle,
} from "@rallly/ui/card";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { Card } from "@/components/card";
import { getPollLayout } from "@/components/layouts/poll-layout";
import { PayWall } from "@/components/pay-wall";
import { FinalizePollForm } from "@/components/poll/manage-poll/finalize-poll-dialog";
import { Trans } from "@/components/trans";
@ -18,7 +18,6 @@ import { usePoll } from "@/contexts/poll";
import { NextPageWithLayout } from "@/types";
import { usePostHog } from "@/utils/posthog";
import { trpc } from "@/utils/trpc/client";
import { getStaticTranslations } from "@/utils/with-page-translations";
const FinalizationForm = () => {
const plan = usePlan();
@ -94,15 +93,4 @@ const Page: NextPageWithLayout = () => {
);
};
Page.getLayout = getPollLayout;
export const getStaticPaths = async () => {
return {
paths: [],
fallback: "blocking",
};
};
export const getStaticProps = getStaticTranslations;
export default Page;

View 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,
};
}

View file

@ -1,17 +1,14 @@
import { InfoIcon } from "@rallly/icons";
"use client";
import { cn } from "@rallly/ui";
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
import { InfoIcon } from "lucide-react";
import { Trans } from "next-i18next";
import { getPollLayout } from "@/components/layouts/poll-layout";
import { LoginLink } from "@/components/login-link";
import { Poll } from "@/components/poll";
import { RegisterLink } from "@/components/register-link";
import { useUser } from "@/components/user-provider";
import { usePoll } from "@/contexts/poll";
import { NextPageWithLayout } from "@/types";
import { isSelfHosted } from "@/utils/constants";
import { getStaticTranslations } from "@/utils/with-page-translations";
const GuestPollAlert = () => {
const poll = usePoll();
@ -49,25 +46,11 @@ const GuestPollAlert = () => {
);
};
const Page: NextPageWithLayout = () => {
export default function Page() {
return (
<div className={cn("mx-auto w-full max-w-4xl space-y-3 sm:space-y-4")}>
<GuestPollAlert />
<Poll />
</div>
);
};
Page.getLayout = getPollLayout;
Page.isAuthRequired = isSelfHosted;
export const getStaticPaths = async () => {
return {
paths: [],
fallback: "blocking",
};
};
export const getStaticProps = getStaticTranslations;
export default Page;
}

View 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"),
};
}

View file

@ -1,19 +1,17 @@
"use client";
import { Button } from "@rallly/ui/button";
import dayjs from "dayjs";
import {
InboxIcon,
PauseCircleIcon,
PlusIcon,
RadioIcon,
VoteIcon,
} from "@rallly/icons";
import { Button } from "@rallly/ui/button";
import dayjs from "dayjs";
import Head from "next/head";
} from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { Container } from "@/components/container";
import { DateIcon } from "@/components/date-icon";
import { getStandardLayout } from "@/components/layouts/standard-layout";
import {
TopBar,
TopBarTitle,
@ -22,11 +20,8 @@ import { ParticipantAvatarBar } from "@/components/participant-avatar-bar";
import { PollStatusBadge } from "@/components/poll-status";
import { Skeleton } from "@/components/skeleton";
import { Trans } from "@/components/trans";
import { NextPageWithLayout } from "@/types";
import { isSelfHosted } from "@/utils/constants";
import { useDayjs } from "@/utils/dayjs";
import { trpc } from "@/utils/trpc/client";
import { getStaticTranslations } from "@/utils/with-page-translations";
const EmptyState = () => {
return (
@ -57,16 +52,12 @@ const EmptyState = () => {
);
};
const Page: NextPageWithLayout = () => {
export function PollsPage() {
const { data } = trpc.polls.list.useQuery();
const { t } = useTranslation();
const { adjustTimeZone } = useDayjs();
return (
<div>
<Head>
<title>{t("polls")}</title>
</Head>
<TopBar className="flex items-center justify-between gap-4">
<TopBarTitle title={<Trans i18nKey="polls" />} icon={VoteIcon} />
<div>
@ -185,11 +176,4 @@ const Page: NextPageWithLayout = () => {
</div>
</div>
);
};
Page.getLayout = getStandardLayout;
Page.isAuthRequired = isSelfHosted;
export default Page;
export const getStaticProps = getStaticTranslations;
}

View file

@ -1,16 +1,15 @@
import { ArrowUpRight, CreditCardIcon, SendIcon } from "@rallly/icons";
"use client";
import { Button } from "@rallly/ui/button";
import { Card } from "@rallly/ui/card";
import { Label } from "@rallly/ui/label";
import dayjs from "dayjs";
import { GetStaticProps } from "next";
import { ArrowUpRight, CreditCardIcon, SendIcon } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import Script from "next/script";
import { useTranslation } from "next-i18next";
import { BillingPlans } from "@/components/billing/billing-plans";
import { getProfileLayout } from "@/components/layouts/profile-layout";
import {
Settings,
SettingsContent,
@ -19,12 +18,8 @@ import {
} from "@/components/settings/settings";
import { Trans } from "@/components/trans";
import { useSubscription } from "@/contexts/plan";
import { isSelfHosted } from "@/utils/constants";
import { trpc } from "@/utils/trpc/client";
import { NextPageWithLayout } from "../../types";
import { getStaticTranslations } from "../../utils/with-page-translations";
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -61,11 +56,7 @@ const BillingPortal = () => {
);
};
export const proPlanIdMonthly = process.env
.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string;
export const proPlanIdYearly = process.env
.NEXT_PUBLIC_PRO_PLAN_ID_YEARLY as string;
const proPlanIdMonthly = process.env.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string;
const SubscriptionStatus = () => {
const data = useSubscription();
@ -106,7 +97,7 @@ const LegacyBilling = () => {
}
return (
<Card>
<>
{process.env.NEXT_PUBLIC_PADDLE_VENDOR_ID ? (
<Script
src="https://cdn.paddle.com/paddle/paddle.js"
@ -215,7 +206,104 @@ const LegacyBilling = () => {
</Button>
</div>
) : null}
</Card>
<Card>
<div className="grid gap-4 p-4 sm:grid-cols-2">
<div>
<Label>
<Trans i18nKey="billingStatusState" defaults="Status" />
</Label>
<div>
{(() => {
switch (status) {
case "active":
return (
<Trans i18nKey="billingStatusActive" defaults="Active" />
);
case "paused":
return (
<Trans i18nKey="billingStatusPaused" defaults="Paused" />
);
case "deleted":
return (
<Trans
i18nKey="billingStatusDeleted"
defaults="Cancelled"
/>
);
}
})()}
</div>
</div>
<div>
{status === "deleted" ? (
<Label>
<Trans i18nKey="endDate" defaults="End date" />
</Label>
) : (
<Label>
<Trans i18nKey="dueDate" defaults="Due date" />
</Label>
)}
<div>{dayjs(endDate).format("LL")}</div>
</div>
<div>
<Label>
<Trans i18nKey="billingStatusPlan" defaults="Plan" />
</Label>
<div>
<Trans i18nKey="planPro" />
</div>
</div>
<div>
<Label>
<Trans i18nKey="billingPeriod" defaults="Period" />
</Label>
<div>
{planId === proPlanIdMonthly ? (
<Trans i18nKey="billingPeriodMonthly" defaults="Monthly" />
) : (
<Trans i18nKey="billingPeriodYearly" defaults="Yearly" />
)}
</div>
</div>
</div>
{status === "active" || status === "paused" ? (
<div className="flex items-center gap-x-2 border-t bg-gray-50 p-3">
<Button
asChild
onClick={(e) => {
e.preventDefault();
window.Paddle.Checkout.open({
override: userPaymentData.updateUrl,
});
}}
>
<Link href={userPaymentData.updateUrl}>
<CreditCardIcon className="h-4 w-4" />
<Trans
i18nKey="subscriptionUpdatePayment"
defaults="Update Payment Details"
/>
</Link>
</Button>
<Button
asChild
variant="destructive"
onClick={(e) => {
e.preventDefault();
window.Paddle.Checkout.open({
override: userPaymentData.cancelUrl,
});
}}
>
<Link href={userPaymentData.cancelUrl}>
<Trans i18nKey="subscriptionCancel" defaults="Cancel" />
</Link>
</Button>
</div>
) : null}
</Card>
</>
);
};
@ -241,7 +329,7 @@ const LegacyBilling = () => {
</div>
</SettingsSection>;
const Page: NextPageWithLayout = () => {
export function BillingPage() {
const { t } = useTranslation();
return (
@ -291,18 +379,4 @@ const Page: NextPageWithLayout = () => {
</SettingsContent>
</Settings>
);
};
Page.getLayout = getProfileLayout;
export const getStaticProps: GetStaticProps = async (ctx) => {
// This page is only available on the hosted version
if (isSelfHosted) {
return {
notFound: true,
};
}
return await getStaticTranslations(ctx);
};
export default Page;
}

View 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"),
};
}

View 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>;
}

View file

@ -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"),
};
}

View file

@ -1,7 +1,7 @@
"use client";
import Head from "next/head";
import { useTranslation } from "next-i18next";
import { getProfileLayout } from "@/components/layouts/profile-layout";
import { useTranslation } from "@/app/i18n/client";
import { DateTimePreferences } from "@/components/settings/date-time-preferences";
import { LanguagePreference } from "@/components/settings/language-preference";
import {
@ -12,10 +12,7 @@ import {
} from "@/components/settings/settings";
import { Trans } from "@/components/trans";
import { NextPageWithLayout } from "../../types";
import { getStaticTranslations } from "../../utils/with-page-translations";
const Page: NextPageWithLayout = () => {
export function PreferencesPage() {
const { t } = useTranslation();
return (
@ -52,10 +49,4 @@ const Page: NextPageWithLayout = () => {
</SettingsContent>
</Settings>
);
};
Page.getLayout = getProfileLayout;
export const getStaticProps = getStaticTranslations;
export default Page;
}

View 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"),
};
}

View file

@ -1,7 +1,7 @@
"use client";
import Head from "next/head";
import { useTranslation } from "next-i18next";
import { getProfileLayout } from "@/components/layouts/profile-layout";
import { ProfileSettings } from "@/components/settings/profile-settings";
import {
Settings,
@ -11,10 +11,7 @@ import {
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
import { NextPageWithLayout } from "../../types";
import { getStaticTranslations } from "../../utils/with-page-translations";
const Page: NextPageWithLayout = () => {
export const ProfilePage = () => {
const { t } = useTranslation();
const { user } = useUser();
@ -69,10 +66,3 @@ const Page: NextPageWithLayout = () => {
</Settings>
);
};
Page.getLayout = getProfileLayout;
Page.isAuthRequired = true;
export const getStaticProps = getStaticTranslations;
export default Page;

View 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>;
}

View 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"),
};
}

View 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"),
};
}

View file

@ -1,7 +1,7 @@
"use client";
import { Button } from "@rallly/ui/button";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useParams, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
import { Trans, useTranslation } from "next-i18next";
import { usePostHog } from "posthog-js/react";
@ -9,16 +9,11 @@ import React from "react";
import { useForm } from "react-hook-form";
import { useDefaultEmail, VerifyCode } from "@/components/auth/auth-forms";
import { StandardLayout } from "@/components/layouts/standard-layout";
import { TextInput } from "@/components/text-input";
import { NextPageWithLayout } from "@/types";
import { useDayjs } from "@/utils/dayjs";
import { requiredString, validEmail } from "@/utils/form-validation";
import { trpc } from "@/utils/trpc/client";
import { AuthLayout } from "../components/auth/auth-layout";
import { getStaticTranslations } from "../utils/with-page-translations";
type RegisterFormData = {
name: string;
email: string;
@ -30,7 +25,8 @@ export const RegisterForm: React.FunctionComponent<{
const [defaultEmail, setDefaultEmail] = useDefaultEmail();
const { t } = useTranslation();
const { timeZone } = useDayjs();
const router = useRouter();
const params = useParams<{ locale: string }>();
const searchParams = useSearchParams();
const { register, handleSubmit, getValues, setError, formState } =
useForm<RegisterFormData>({
defaultValues: { email: defaultEmail },
@ -47,7 +43,7 @@ export const RegisterForm: React.FunctionComponent<{
<VerifyCode
onSubmit={async (code) => {
// get user's time zone
const locale = router.locale;
const locale = params?.locale ?? "en";
const res = await authenticateRegistration.mutateAsync({
token,
timeZone,
@ -72,7 +68,7 @@ export const RegisterForm: React.FunctionComponent<{
signIn("registration-token", {
token,
callbackUrl: router.query.callbackUrl as string,
callbackUrl: searchParams?.get("callbackUrl") ?? undefined,
});
}}
onChange={() => setToken(undefined)}
@ -181,24 +177,3 @@ export const RegisterForm: React.FunctionComponent<{
</form>
);
};
const Page: NextPageWithLayout = () => {
const { t } = useTranslation();
return (
<AuthLayout>
<Head>
<title>{t("register")}</title>
</Head>
<RegisterForm />
</AuthLayout>
);
};
Page.getLayout = (page) => {
return <StandardLayout hideNav={true}>{page}</StandardLayout>;
};
export const getStaticProps = getStaticTranslations;
export default Page;

View 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}`)}
/>
);
}

View file

@ -0,0 +1,5 @@
import { Params } from "@/app/[locale]/types";
export interface PParams extends Params {
adminUrlId: string;
}

View file

@ -1,34 +1,19 @@
"use client";
import { Button } from "@rallly/ui/button";
import { useMutation } from "@tanstack/react-query";
import { GetServerSideProps } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { useTranslation } from "next-i18next";
import { z } from "zod";
import { StandardLayout } from "@/components/layouts/standard-layout";
import { Logo } from "@/components/logo";
import { Skeleton } from "@/components/skeleton";
import { Trans } from "@/components/trans";
import { UserAvatar } from "@/components/user";
import { NextPageWithLayout } from "@/types";
import { usePostHog } from "@/utils/posthog";
import { trpc } from "@/utils/trpc/client";
import { getServerSideTranslations } from "@/utils/with-page-translations";
const params = z.object({
magicLink: z.string().url(),
});
const magicLinkParams = z.object({
email: z.string().email(),
token: z.string(),
});
type PageProps = { magicLink: string; email: string };
const Page: NextPageWithLayout<PageProps> = ({ magicLink, email }) => {
export const LoginPage = ({ magicLink, email }: PageProps) => {
const session = useSession();
const posthog = usePostHog();
const trpcUtils = trpc.useUtils();
@ -61,12 +46,8 @@ const Page: NextPageWithLayout<PageProps> = ({ magicLink, email }) => {
});
const { data } = trpc.user.getByEmail.useQuery({ email });
const router = useRouter();
const { t } = useTranslation();
return (
<div className="flex h-screen flex-col items-center justify-center gap-4 p-4">
<Head>
<title>{t("login")}</title>
</Head>
<div className="mb-6">
<Logo />
</div>
@ -105,43 +86,3 @@ const Page: NextPageWithLayout<PageProps> = ({ magicLink, email }) => {
</div>
);
};
Page.getLayout = (page) => (
<StandardLayout hideNav={true}>{page}</StandardLayout>
);
export const getServerSideProps: GetServerSideProps<PageProps> = async (
ctx,
) => {
const parse = params.safeParse(ctx.query);
if (!parse.success) {
return {
notFound: true,
};
}
const { magicLink } = parse.data;
const url = new URL(magicLink);
const parseMagicLink = magicLinkParams.safeParse(
Object.fromEntries(url.searchParams),
);
if (!parseMagicLink.success) {
return {
notFound: true,
};
}
return {
props: {
magicLink,
email: parseMagicLink.data.email,
...(await getServerSideTranslations(ctx)),
},
};
};
export default Page;

View 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>
);
}

View 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"),
};
}

View 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;
}

View 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>
);
}

View 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>
);
}

View 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;
}

View 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>
);
}

View 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}`)}
/>
);
}

View file

@ -0,0 +1,5 @@
import { Params } from "@/app/[locale]/types";
export interface PParams extends Params {
participantUrlId: string;
}

View file

@ -0,0 +1,3 @@
export interface Params {
locale: string;
}

View 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
View 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,
};
}

View 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>
);
}

View 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,
};
}

View 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>
);
}

View file

@ -1,6 +1,7 @@
"use client";
import { Button } from "@rallly/ui/button";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import { signIn, useSession } from "next-auth/react";
import { Trans, useTranslation } from "next-i18next";
import React from "react";
@ -133,7 +134,7 @@ export const LoginForm: React.FunctionComponent<{
const [email, setEmail] = React.useState<string>();
const posthog = usePostHog();
const router = useRouter();
const callbackUrl = (router.query.callbackUrl as string) ?? "/";
const callbackUrl = (useSearchParams()?.get("callbackUrl") as string) ?? "/";
const sendVerificationEmail = (email: string) => {
return signIn("email", {

View file

@ -1,4 +1,3 @@
import { TrendingUpIcon } from "@rallly/icons";
import {
BillingPlan,
BillingPlanDescription,
@ -11,6 +10,7 @@ import {
} from "@rallly/ui/billing-plan";
import { Button } from "@rallly/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
import { TrendingUpIcon } from "lucide-react";
import React from "react";
import { Trans } from "@/components/trans";

View file

@ -1,4 +1,3 @@
import { GlobeIcon } from "@rallly/icons";
import { cn } from "@rallly/ui";
import {
Dialog,
@ -10,6 +9,7 @@ import {
} from "@rallly/ui/dialog";
import { Label } from "@rallly/ui/label";
import dayjs from "dayjs";
import { GlobeIcon } from "lucide-react";
import React from "react";
import { useInterval } from "react-use";
import spacetime from "spacetime";

View file

@ -1,3 +1,4 @@
"use client";
import { Button } from "@rallly/ui/button";
import {
Card,
@ -7,7 +8,7 @@ import {
CardTitle,
} from "@rallly/ui/card";
import { Form } from "@rallly/ui/form";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import React from "react";
import { useForm } from "react-hook-form";
import useFormPersist from "react-hook-form-persist";

View file

@ -1,8 +1,3 @@
import {
MessageCircleIcon,
MoreHorizontalIcon,
TrashIcon,
} from "@rallly/icons";
import { Button } from "@rallly/ui/button";
import {
DropdownMenu,
@ -12,6 +7,7 @@ import {
} from "@rallly/ui/dropdown-menu";
import { Textarea } from "@rallly/ui/textarea";
import dayjs from "dayjs";
import { MessageCircleIcon, MoreHorizontalIcon, TrashIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";

View file

@ -1,6 +1,5 @@
import { FrownIcon } from "@rallly/icons";
import { Button } from "@rallly/ui/button";
import Head from "next/head";
import { FrownIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import * as React from "react";
@ -19,9 +18,6 @@ const ErrorPage: React.FunctionComponent<ComponentProps> = ({
const { t } = useTranslation();
return (
<div className="flex h-[calc(100vh-100px)] w-full items-center justify-center">
<Head>
<title>{title}</title>
</Head>
<div className="space-y-8">
<div className="space-y-4 text-center">
<Icon className="mb-4 inline-block h-24 w-24 text-gray-400" />

View file

@ -1,5 +1,5 @@
import { MapPinIcon, MousePointerClickIcon, TextIcon } from "@rallly/icons";
import dayjs from "dayjs";
import { MapPinIcon, MousePointerClickIcon, TextIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import { Card } from "@/components/card";

View file

@ -1,7 +1,7 @@
import { HelpCircleIcon } from "@rallly/icons";
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
import { HelpCircleIcon } from "lucide-react";
import Script from "next/script";
import React from "react";

View file

@ -1,10 +1,3 @@
import {
BugIcon,
LifeBuoyIcon,
LightbulbIcon,
MegaphoneIcon,
SmileIcon,
} from "@rallly/icons";
import {
DropdownMenu,
DropdownMenuContent,
@ -13,6 +6,13 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@rallly/ui/dropdown-menu";
import {
BugIcon,
LifeBuoyIcon,
LightbulbIcon,
MegaphoneIcon,
SmileIcon,
} from "lucide-react";
import Link from "next/link";
import { Trans } from "next-i18next";

View file

@ -1,5 +1,5 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@rallly/icons";
import { Button } from "@rallly/ui/button";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import * as React from "react";

View file

@ -1,12 +1,3 @@
import {
CalendarIcon,
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
PlusIcon,
SparklesIcon,
XIcon,
} from "@rallly/icons";
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import {
@ -20,6 +11,15 @@ import {
import { Switch } from "@rallly/ui/switch";
import clsx from "clsx";
import dayjs from "dayjs";
import {
CalendarIcon,
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
PlusIcon,
SparklesIcon,
XIcon,
} from "lucide-react";
import { useTranslation } from "next-i18next";
import * as React from "react";

View file

@ -6,9 +6,9 @@ import {
useFloating,
} from "@floating-ui/react-dom-interactions";
import { Listbox } from "@headlessui/react";
import { ChevronDownIcon } from "@rallly/icons";
import clsx from "clsx";
import dayjs from "dayjs";
import { ChevronDownIcon } from "lucide-react";
import * as React from "react";
import { getDuration } from "@/utils/date-time-utils";

View file

@ -1,7 +1,7 @@
import { CalendarIcon, TableIcon } from "@rallly/icons";
import { Card, CardDescription, CardHeader, CardTitle } from "@rallly/ui/card";
import { FormField, FormMessage } from "@rallly/ui/form";
import { Tabs, TabsList, TabsTrigger } from "@rallly/ui/tabs";
import { CalendarIcon, TableIcon } from "lucide-react";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import { useFormContext } from "react-hook-form";

View file

@ -1,5 +1,5 @@
import { XIcon } from "@rallly/icons";
import dayjs from "dayjs";
import { XIcon } from "lucide-react";
import React from "react";
import { Calendar } from "react-big-calendar";
import { createBreakpoint } from "react-use";
@ -98,7 +98,7 @@ const WeekCalendar: React.FunctionComponent<DateTimePickerProps> = ({
width: `calc(${props.style?.width}%)`,
}}
>
<div className="absolute top-1.5 right-1.5 flex justify-end opacity-0 group-hover:opacity-100">
<div className="absolute right-1.5 top-1.5 flex justify-end opacity-0 group-hover:opacity-100">
<XIcon className="h-3 w-3" />
</div>
<div>

View file

@ -1,10 +1,3 @@
import {
AtSignIcon,
EyeIcon,
InfoIcon,
MessageCircleIcon,
VoteIcon,
} from "@rallly/icons";
import { cn } from "@rallly/ui";
import {
Card,
@ -16,6 +9,13 @@ import {
import { FormField } from "@rallly/ui/form";
import { Switch } from "@rallly/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
import {
AtSignIcon,
EyeIcon,
InfoIcon,
MessageCircleIcon,
VoteIcon,
} from "lucide-react";
import React from "react";
import { useFormContext } from "react-hook-form";
import { Trans } from "react-i18next";

View file

@ -1,4 +1,3 @@
import { ArrowUpRightIcon, Share2Icon } from "@rallly/icons";
import { Button } from "@rallly/ui/button";
import {
Dialog,
@ -8,6 +7,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@rallly/ui/dialog";
import { ArrowUpRightIcon, Share2Icon } from "lucide-react";
import Link from "next/link";
import React from "react";
import { useCopyToClipboard } from "react-use";

View file

@ -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>;
};

View file

@ -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>
</>
);
};

View file

@ -1,3 +1,12 @@
"use client";
import { Button } from "@rallly/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIconLabel,
DropdownMenuTrigger,
} from "@rallly/ui/dropdown-menu";
import {
ArrowLeftIcon,
ArrowUpRight,
@ -9,18 +18,10 @@ import {
PlayCircleIcon,
RotateCcw,
ShieldCloseIcon,
} from "@rallly/icons";
import { Button } from "@rallly/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIconLabel,
DropdownMenuTrigger,
} from "@rallly/ui/dropdown-menu";
} from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useParams, usePathname } from "next/navigation";
import React from "react";
import { Container } from "@/components/container";
@ -46,7 +47,6 @@ import { Skeleton } from "@/components/skeleton";
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
import { usePoll } from "@/contexts/poll";
import Error404 from "@/pages/404";
import { trpc } from "@/utils/trpc/client";
import { NextPageWithLayout } from "../../types";
@ -155,12 +155,12 @@ const StatusControl = () => {
const AdminControls = () => {
const poll = usePoll();
const pollLink = `/poll/${poll.id}`;
const router = useRouter();
const pathname = usePathname();
return (
<TopBar>
<div className="flex flex-col items-start justify-between gap-x-4 gap-y-2 sm:flex-row">
<div className="flex min-w-0 gap-4">
{router.asPath !== pollLink ? (
{pathname !== pollLink ? (
<Button asChild>
<Link href={pollLink}>
<ArrowLeftIcon className="h-4 w-4" />
@ -257,18 +257,14 @@ const Title = () => {
};
const Prefetch = ({ children }: React.PropsWithChildren) => {
const router = useRouter();
const params = useParams();
const urlId = router.query.urlId as string;
const urlId = params?.urlId as string;
const poll = trpc.polls.get.useQuery({ urlId });
const participants = trpc.polls.participants.list.useQuery({ pollId: urlId });
const watchers = trpc.polls.getWatchers.useQuery({ pollId: urlId });
if (poll.error?.data?.code === "NOT_FOUND") {
return <Error404 />;
}
if (!poll.data || !watchers.data || !participants.data) {
return (
<div>
@ -287,10 +283,10 @@ const Prefetch = ({ children }: React.PropsWithChildren) => {
return <>{children}</>;
};
const PollLayout = ({ children }: React.PropsWithChildren) => {
const router = useRouter();
export const PollLayout = ({ children }: React.PropsWithChildren) => {
const params = useParams();
const urlId = router.query.urlId as string;
const urlId = params?.urlId as string;
if (!urlId) {
// probably navigating away

View file

@ -1,25 +1,24 @@
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import { Card } from "@rallly/ui/card";
import clsx from "clsx";
import {
CreditCardIcon,
MenuIcon,
Settings2Icon,
UserIcon,
} from "@rallly/icons";
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import { Card } from "@rallly/ui/card";
import clsx from "clsx";
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import React from "react";
import { Trans } from "react-i18next";
import { useToggle } from "react-use";
import { Container } from "@/components/container";
import { StandardLayout } from "@/components/layouts/standard-layout";
import { IfCloudHosted } from "@/contexts/environment";
import { Plan } from "@/contexts/plan";
import { IconComponent, NextPageWithLayout } from "../../types";
import { IconComponent } from "../../types";
import { IfAuthenticated, useUser } from "../user-provider";
const MenuItem = (props: {
@ -27,12 +26,12 @@ const MenuItem = (props: {
href: string;
children: React.ReactNode;
}) => {
const router = useRouter();
const pathname = usePathname();
return (
<Link
className={clsx(
"flex min-w-0 items-center gap-x-2.5 px-2.5 py-1.5 text-sm font-medium",
router.asPath === props.href
pathname === props.href
? "bg-gray-200"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-800",
)}
@ -48,13 +47,13 @@ export const ProfileLayout = ({ children }: React.PropsWithChildren) => {
const { user } = useUser();
// reset toggle whenever route changes
const router = useRouter();
const pathname = usePathname();
const [isMenuOpen, toggle] = useToggle(false);
React.useEffect(() => {
toggle(false);
}, [router.asPath, toggle]);
}, [pathname, toggle]);
return (
<div>
@ -101,12 +100,3 @@ export const ProfileLayout = ({ children }: React.PropsWithChildren) => {
</div>
);
};
export const getProfileLayout: NextPageWithLayout["getLayout"] =
function getLayout(page) {
return (
<StandardLayout>
<ProfileLayout>{page}</ProfileLayout>
</StandardLayout>
);
};

View file

@ -1,10 +1,11 @@
import { ListIcon, SparklesIcon } from "@rallly/icons";
"use client";
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import clsx from "clsx";
import { AnimatePresence, m } from "framer-motion";
import { ListIcon, SparklesIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import React from "react";
import { Toaster } from "react-hot-toast";
@ -17,7 +18,6 @@ import {
import FeedbackButton from "@/components/feedback";
import { LoginLink } from "@/components/login-link";
import { Logo } from "@/components/logo";
import { Spinner } from "@/components/spinner";
import { Trans } from "@/components/trans";
import { UserDropdown } from "@/components/user-dropdown";
import { IfCloudHosted } from "@/contexts/environment";
@ -41,14 +41,14 @@ const NavMenuItem = ({
label: React.ReactNode;
className?: string;
}) => {
const router = useRouter();
const pathname = usePathname();
return (
<Button variant="ghost" asChild>
<Link
target={target}
href={href}
className={cn(
router.asPath === href
pathname === href
? "text-foreground"
: "text-muted-foreground hover:text-foreground active:bg-gray-200/50",
className,
@ -78,18 +78,6 @@ const Upgrade = () => {
};
const LogoArea = () => {
const router = useRouter();
const [isBusy, setIsBusy] = React.useState(false);
React.useEffect(() => {
const setBusy = () => setIsBusy(true);
const setNotBusy = () => setIsBusy(false);
router.events.on("routeChangeStart", setBusy);
router.events.on("routeChangeComplete", setNotBusy);
return () => {
router.events.off("routeChangeStart", setBusy);
router.events.off("routeChangeComplete", setNotBusy);
};
}, [router.events]);
return (
<div className="relative flex items-center justify-center gap-x-4">
<Link
@ -100,14 +88,6 @@ const LogoArea = () => {
>
<Logo size="sm" />
</Link>
<div
className={cn(
"pointer-events-none flex w-5 items-center justify-center text-gray-500 transition-opacity delay-500",
isBusy ? "opacity-100" : "opacity-0",
)}
>
{isBusy ? <Spinner /> : null}
</div>
</div>
);
};

View file

@ -1,5 +1,5 @@
import Link, { LinkProps } from "next/link";
import { useRouter } from "next/router";
import { usePathname, useRouter } from "next/navigation";
import React from "react";
export const LoginLink = React.forwardRef<
@ -7,6 +7,7 @@ export const LoginLink = React.forwardRef<
React.PropsWithChildren<Omit<LinkProps, "href"> & { className?: string }>
>(function LoginLink({ children, ...props }, ref) {
const router = useRouter();
const pathname = usePathname() ?? "/";
return (
<Link
ref={ref}
@ -15,7 +16,7 @@ export const LoginLink = React.forwardRef<
onClick={async (e) => {
e.preventDefault();
props.onClick?.(e);
router.push("/login?callbackUrl=" + encodeURIComponent(router.asPath));
router.push("/login?callbackUrl=" + encodeURIComponent(pathname));
}}
>
{children}

View file

@ -1,6 +1,6 @@
import { Dialog } from "@headlessui/react";
import { XIcon } from "@rallly/icons";
import { AnimatePresence, m } from "framer-motion";
import { XIcon } from "lucide-react";
import * as React from "react";
import { ButtonProps, LegacyButton } from "../button";

View file

@ -1,4 +1,3 @@
import { PencilIcon, TagIcon, TrashIcon } from "@rallly/icons";
import { Button } from "@rallly/ui/button";
import {
Dialog,
@ -27,6 +26,7 @@ import {
FormMessage,
} from "@rallly/ui/form";
import { Input } from "@rallly/ui/input";
import { PencilIcon, TagIcon, TrashIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import React from "react";
import { SubmitHandler, useForm } from "react-hook-form";

View file

@ -1,3 +1,8 @@
import { cn } from "@rallly/ui";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
import { m } from "framer-motion";
import {
CalendarCheck2Icon,
CopyIcon,
@ -7,14 +12,9 @@ import {
LockIcon,
Settings2Icon,
TrendingUpIcon,
} from "@rallly/icons";
import { cn } from "@rallly/ui";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
import { m } from "framer-motion";
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useParams } from "next/navigation";
import React from "react";
import { Trans } from "@/components/trans";
@ -51,7 +51,7 @@ const Feature = ({
};
const Teaser = () => {
const router = useRouter();
const params = useParams();
const [tab, setTab] = React.useState("yearly");
@ -213,7 +213,7 @@ const Teaser = () => {
<Trans i18nKey="upgrade" defaults="Upgrade" />
</UpgradeButton>
<Button asChild className="w-full">
<Link href={`/poll/${router.query.urlId as string}`}>
<Link href={`/poll/${params?.urlId as string}`}>
<Trans i18nKey="notToday" defaults="Not Today" />
</Link>
</Button>

View file

@ -1,6 +1,6 @@
import { Participant, VoteType } from "@rallly/database";
import { TrashIcon } from "@rallly/icons";
import { keyBy } from "lodash";
import { TrashIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import React from "react";

View file

@ -1,5 +1,5 @@
import { CheckCircleIcon, PauseCircleIcon, RadioIcon } from "@rallly/icons";
import { cn } from "@rallly/ui";
import { CheckCircleIcon, PauseCircleIcon, RadioIcon } from "lucide-react";
import { Trans } from "@/components/trans";
import { IconComponent } from "@/types";

View file

@ -1,3 +1,6 @@
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
import {
ArrowLeftIcon,
ArrowRightIcon,
@ -5,10 +8,7 @@ import {
PlusIcon,
ShrinkIcon,
Users2Icon,
} from "@rallly/icons";
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
} from "lucide-react";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import { RemoveScroll } from "react-remove-scroll";

View file

@ -1,7 +1,7 @@
import { Participant, VoteType } from "@rallly/database";
import { MoreHorizontalIcon } from "@rallly/icons";
import { Button } from "@rallly/ui/button";
import clsx from "clsx";
import { MoreHorizontalIcon } from "lucide-react";
import * as React from "react";
import { ParticipantDropdown } from "@/components/participant-dropdown";

Some files were not shown because too many files have changed in this diff Show more