mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-14 17:36:49 +02:00
Load locale ondemand + spanish locale (#249)
This commit is contained in:
parent
0f35bd0518
commit
c2aea134ef
30 changed files with 700 additions and 455 deletions
2
declarations/i18next.d.ts
vendored
2
declarations/i18next.d.ts
vendored
|
@ -2,6 +2,7 @@ import "react-i18next";
|
||||||
|
|
||||||
import app from "~/public/locales/en/app.json";
|
import app from "~/public/locales/en/app.json";
|
||||||
import common from "~/public/locales/en/common.json";
|
import common from "~/public/locales/en/common.json";
|
||||||
|
import errors from "~/public/locales/en/errors.json";
|
||||||
import homepage from "~/public/locales/en/homepage.json";
|
import homepage from "~/public/locales/en/homepage.json";
|
||||||
|
|
||||||
declare module "next-i18next" {
|
declare module "next-i18next" {
|
||||||
|
@ -9,5 +10,6 @@ declare module "next-i18next" {
|
||||||
homepage: typeof homepage;
|
homepage: typeof homepage;
|
||||||
app: typeof app;
|
app: typeof app;
|
||||||
common: typeof common;
|
common: typeof common;
|
||||||
|
errors: typeof errors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ const path = require("path");
|
||||||
module.exports = {
|
module.exports = {
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
locales: ["en", "de", "fr", "sv"],
|
locales: ["en", "es", "de", "fr", "sv"],
|
||||||
localePath: path.resolve("./public/locales"),
|
localePath: path.resolve("./public/locales"),
|
||||||
reloadOnPrerender: process.env.NODE_ENV === "development",
|
reloadOnPrerender: process.env.NODE_ENV === "development",
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,5 +17,9 @@
|
||||||
"starOnGithub": "Star us on Github",
|
"starOnGithub": "Star us on Github",
|
||||||
"support": "Support",
|
"support": "Support",
|
||||||
"swedish": "Swedish",
|
"swedish": "Swedish",
|
||||||
"volunteerTranslator": "Help translate this site"
|
"volunteerTranslator": "Help translate this site",
|
||||||
|
"notFoundTitle": "404 not found",
|
||||||
|
"notFoundDescription": "We couldn't find the page you're looking for.",
|
||||||
|
"goToHome": "Go to home",
|
||||||
|
"startChat": "Start chat"
|
||||||
}
|
}
|
||||||
|
|
6
public/locales/en/errors.json
Normal file
6
public/locales/en/errors.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"notFoundTitle": "404 not found",
|
||||||
|
"notFoundDescription": "We couldn't find the page you're looking for.",
|
||||||
|
"goToHome": "Go to home",
|
||||||
|
"startChat": "Start chat"
|
||||||
|
}
|
128
public/locales/es/app.json
Normal file
128
public/locales/es/app.json
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
{
|
||||||
|
"12h": "12h",
|
||||||
|
"24h": "24h",
|
||||||
|
"addParticipant": "Añadir participante",
|
||||||
|
"addTimeOption": "Añadir hora",
|
||||||
|
"alreadyVoted": "Ya has votado",
|
||||||
|
"applyToAllDates": "Aplicar a todas las fechas",
|
||||||
|
"areYouSure": "¿Estás seguro?",
|
||||||
|
"back": "Volver",
|
||||||
|
"calendarHelp": "No puedes crear una encuesta sin ninguna opción. Añada al menos una opción para continuar.",
|
||||||
|
"calendarHelpTitle": "¿Olvidaste algo?",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"comment": "Comentario",
|
||||||
|
"commentPlaceholder": "Dejar un comentario en esta encuesta (visible para todos)",
|
||||||
|
"comments": "Comentarios",
|
||||||
|
"continue": "Continuar",
|
||||||
|
"copied": "Copiado",
|
||||||
|
"copyLink": "Copiar enlace",
|
||||||
|
"createdBy": "de <b>{{name}}</b>",
|
||||||
|
"createPoll": "Crear encuesta",
|
||||||
|
"creatingDemo": "Creando una encuesta de demostración…",
|
||||||
|
"delete": "Borrar",
|
||||||
|
"deleteComment": "Borrar comentario",
|
||||||
|
"deleteDate": "Borrar fecha",
|
||||||
|
"deletedPoll": "Encuesta borrada",
|
||||||
|
"deletedPollInfo": "Esta encuesta ya no existe.",
|
||||||
|
"deletePoll": "Borrar encuesta",
|
||||||
|
"deletePollDescription": "Todos los datos relacionados con esta encuesta se eliminarán. Para confirmar, por favor escribe <s>“{{confirmText}}”</s> en el campo siguiente:",
|
||||||
|
"deletingOptionsWarning": "Estás eliminando opciones por las que algunos participantes han votado. También se eliminarán sus votos.",
|
||||||
|
"demoPollNotice": "Las encuestas de demostración se eliminan automáticamente después de 1 día",
|
||||||
|
"description": "Descripción",
|
||||||
|
"descriptionPlaceholder": "¡Hola a todos, por favor elijan las fechas que les convengan!",
|
||||||
|
"donate": "Donar",
|
||||||
|
"editDetails": "Editar detalles",
|
||||||
|
"editOptions": "Editar opciones",
|
||||||
|
"email": "Correo electrónico",
|
||||||
|
"emailPlaceholder": "jessie.smith@email.com",
|
||||||
|
"endingGuestSessionNotice": "Una vez que finalices la sesión de visitante, no se puede reanudar. No podrás editar ningún voto o comentario que hayas hecho con esta sesión.",
|
||||||
|
"endSession": "Cerrar Sesión",
|
||||||
|
"errorCreate": "¡Oh! Hubo un problema al crear tu encuesta. El error ha sido registrado y vamos a intentar arreglarlo.",
|
||||||
|
"exportToCsv": "Exportar a CSV",
|
||||||
|
"finish": "Finalizar",
|
||||||
|
"forgetMe": "Olvídame",
|
||||||
|
"goToAdmin": "Ir al panel de administración",
|
||||||
|
"guest": "Visitante",
|
||||||
|
"guestSessionNotice": "Estás usando una sesión de visitante. Esto nos permite reconocerte si vuelves más tarde para que puedas editar tus votos.",
|
||||||
|
"guestSessionReadMore": "Lee más sobre sesiones de visitantes.",
|
||||||
|
"hide": "Ocultar",
|
||||||
|
"ifNeedBe": "Si es necesario",
|
||||||
|
"linkHasExpired": "Tu enlace ha expirado o ya no es válido",
|
||||||
|
"loading": "Cargando…",
|
||||||
|
"loadingParticipants": "Cargando participantes…",
|
||||||
|
"location": "Ubicación",
|
||||||
|
"locationPlaceholder": "Café de Carlos",
|
||||||
|
"lockPoll": "Bloquear encuesta",
|
||||||
|
"login": "Iniciar sesión",
|
||||||
|
"loginCheckInbox": "Por favor, revisa tus correos electrónicos.",
|
||||||
|
"loginMagicLinkSent": "Se ha enviado un enlace mágico a:",
|
||||||
|
"loginSendMagicLink": "Enviarme un enlace mágico",
|
||||||
|
"loginViaMagicLink": "Iniciar sesión a través de un enlace mágico",
|
||||||
|
"loginViaMagicLinkDescription": "Te enviaremos un correo electrónico con un enlace mágico que puedes usar para iniciar sesión.",
|
||||||
|
"loginWithValidEmail": "Por favor ingresa un correo electrónico válido",
|
||||||
|
"logout": "Cerrar sesión",
|
||||||
|
"manage": "Gestionar",
|
||||||
|
"menu": "Menú",
|
||||||
|
"mixedOptionsDescription": "No puedes tener opciones de fecha y opciones de hora en la misma encuesta. ¿Cuáles quieres mantener?",
|
||||||
|
"mixedOptionsKeepDates": "Mantener las opciones de fecha",
|
||||||
|
"mixedOptionsKeepTimes": "Mantener las opciones de hora",
|
||||||
|
"mixedOptionsTitle": "Aguanta un minuto…🤔",
|
||||||
|
"monday": "Lunes",
|
||||||
|
"monthView": "Vista mensual",
|
||||||
|
"name": "Nombre",
|
||||||
|
"namePlaceholder": "Jessie Smith",
|
||||||
|
"newPoll": "Nueva encuesta",
|
||||||
|
"next": "Siguiente",
|
||||||
|
"nextMonth": "Siguiente mes",
|
||||||
|
"no": "No",
|
||||||
|
"noDatesSelected": "Ninguna fecha seleccionada",
|
||||||
|
"notificationsDisabled": "Las notificaciones han sido desactivadas",
|
||||||
|
"notificationsOff": "Las notificaciones están desactivadas",
|
||||||
|
"notificationsOn": "Las notificaciones están activadas",
|
||||||
|
"notificationsOnDescription": "Vamos a mandar un correo electrónico a <b>{{email}}</b> cuando haya actividad en esta encuesta.",
|
||||||
|
"notificationsVerifyEmail": "Tienes que verificar tu correo electrónico para activar las notificaciones",
|
||||||
|
"ok": "Aceptar",
|
||||||
|
"options": "Opciones",
|
||||||
|
"participant": "Participante",
|
||||||
|
"participantCount_other": "{{count}} participantes",
|
||||||
|
"participantCount": "{{count}} participante",
|
||||||
|
"pollHasBeenLocked": "Esta encuesta ha sido bloqueada",
|
||||||
|
"pollHasBeenVerified": "Esta encuesta ha sido verificada",
|
||||||
|
"pollOwnerNotice": "Hola {{name}}, parece que eres el dueño de esta encuesta.",
|
||||||
|
"pollsEmpty": "Ninguna encuesta creada",
|
||||||
|
"possibleAnswers": "Respuestas posibles",
|
||||||
|
"preferences": "Ajustes",
|
||||||
|
"previousMonth": "Mes anterior",
|
||||||
|
"profileLogin": "Perfil - Iniciar sesión",
|
||||||
|
"profileUser": "Perfil - {{username}}",
|
||||||
|
"requiredNameError": "El nombre es obligatorio",
|
||||||
|
"save": "Guardar",
|
||||||
|
"saveInstruction": "Selecciona tu disponibilidad y haz clic en <b>{{save}}</b>",
|
||||||
|
"share": "Compartir",
|
||||||
|
"shareDescription": "Dale este enlace a tus <b>participantes</b> para permitirles votar en tu encuesta.",
|
||||||
|
"shareLink": "Compartir con un enlace",
|
||||||
|
"specifyTimes": "Especificar tiempos",
|
||||||
|
"specifyTimesDescription": "Incluir horas de inicio y fin para cada opción",
|
||||||
|
"stepSummary": "Paso {{current}} de {{total}}",
|
||||||
|
"sunday": "Domingo",
|
||||||
|
"timeAndDate": "Fecha y hora",
|
||||||
|
"timeFormat": "Formato de hora:",
|
||||||
|
"timeZone": "Zona horaria:",
|
||||||
|
"title": "Título",
|
||||||
|
"titlePlaceholder": "Reunión mensual",
|
||||||
|
"today": "Hoy",
|
||||||
|
"unlockPoll": "Desbloquear encuesta",
|
||||||
|
"unverifiedMessage": "Se ha enviado un correo electrónico a <b>{{email}}</b> con un enlace para verificar la dirección de correo electrónico.",
|
||||||
|
"user": "Usuario",
|
||||||
|
"vote": "Votar",
|
||||||
|
"voteCount_other": "{{count}} votos",
|
||||||
|
"voteCount": "{{count}} voto",
|
||||||
|
"weekStartsOn": "La semana comienza el",
|
||||||
|
"weekView": "Vista semanal",
|
||||||
|
"whatsThis": "¿Qué es esto?",
|
||||||
|
"yes": "Sí",
|
||||||
|
"you": "Tú",
|
||||||
|
"yourDetails": "Tus datos",
|
||||||
|
"yourName": "Tu nombre…",
|
||||||
|
"yourPolls": "Tus encuestas"
|
||||||
|
}
|
21
public/locales/es/common.json
Normal file
21
public/locales/es/common.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"blog": "Blog",
|
||||||
|
"discussions": "Discusiones",
|
||||||
|
"donate": "Donar",
|
||||||
|
"english": "Inglés",
|
||||||
|
"footerCredit": "Creado por <a>@imlukevella</a>",
|
||||||
|
"footerSponsor": "Este proyecto está financiado por los usuarios. Por favor, considera apoyarlo <a>donando</a>.",
|
||||||
|
"french": "Francés",
|
||||||
|
"german": "Alemán",
|
||||||
|
"home": "Inicio",
|
||||||
|
"italian": "Italiano",
|
||||||
|
"language": "Idioma",
|
||||||
|
"links": "Enlaces",
|
||||||
|
"poweredBy": "Con tecnología de",
|
||||||
|
"privacyPolicy": "Política de privacidad",
|
||||||
|
"spanish": "Español",
|
||||||
|
"starOnGithub": "Seguir el proyecto en GitHub",
|
||||||
|
"support": "Soporte",
|
||||||
|
"swedish": "Sueco",
|
||||||
|
"volunteerTranslator": "Ayuda a traducir esta página"
|
||||||
|
}
|
36
public/locales/es/homepage.json
Normal file
36
public/locales/es/homepage.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"3Ls": "Sí—con 3 <e>L</e>s",
|
||||||
|
"adFree": "Sin anuncios",
|
||||||
|
"adFreeDescription": "Puedes darle un descanso a tu bloqueador de anuncios — No lo necesitarás aquí.",
|
||||||
|
"comments": "Comentarios",
|
||||||
|
"commentsDescription": "Los participantes pueden comentar en tu encuesta y los comentarios serán visibles para todos.",
|
||||||
|
"features": "Características",
|
||||||
|
"featuresSubheading": "Programar reuniones, de una manera inteligente",
|
||||||
|
"follow": "Seguir",
|
||||||
|
"getStarted": "Empezar",
|
||||||
|
"heroSubText": "Encuentra la fecha correcta sin dar vueltas",
|
||||||
|
"heroText": "Programar<br/><s>reuniones</s><br />con facilidad",
|
||||||
|
"links": "Enlaces",
|
||||||
|
"liveDemo": "Demostración en vivo",
|
||||||
|
"metaDescription": "Crea encuestas y vota para encontrar el mejor día o la mejor hora. Una alternativa gratuita a Doodle.",
|
||||||
|
"metaTitle": "Rallly - Programar reuniones",
|
||||||
|
"mobileFriendly": "Optimizado para dispositivos móviles",
|
||||||
|
"mobileFriendlyDescription": "Funciona muy bien en dispositivos móviles para que los participantes puedan responder a las encuestas dondequiera que estén.",
|
||||||
|
"new": "Nuevo",
|
||||||
|
"noLoginRequired": "No es necesario iniciar sesión",
|
||||||
|
"noLoginRequiredDescription": "No es necesario iniciar sesión para crear o participar en una encuesta",
|
||||||
|
"notifications": "Notificaciones",
|
||||||
|
"notificationsDescription": "Sabe quién ha respondido. Recibe notificaciones cuando los participantes voten o comenten en tu encuesta.",
|
||||||
|
"openSource": "Código abierto",
|
||||||
|
"openSourceDescription": "Ese proyecto es completamente de código abierto y <a>disponible en GitHub</a>.",
|
||||||
|
"participant": "Participante",
|
||||||
|
"participantCount_other": "{{count}} participantes",
|
||||||
|
"participantCount": "{{count}} participante",
|
||||||
|
"perfect": "¡Perfecto!",
|
||||||
|
"principles": "Principios",
|
||||||
|
"principlesSubheading": "No somos como los otros",
|
||||||
|
"selfHostable": "Autoalojable",
|
||||||
|
"selfHostableDescription": "Ejecútalo en tu propio servidor para tener el control total de tus datos",
|
||||||
|
"timeSlots": "Intervalos de tiempo",
|
||||||
|
"timeSlotsDescription": "Establece la hora de inicio y fin individual para cada opción de tu encuesta. Los tiempos se pueden ajustar automáticamente a la zona horaria de cada participante o pueden ser ajustados para ignorar completamente las zonas horarias."
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { usePlausible } from "next-plausible";
|
import { usePlausible } from "next-plausible";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { useDayjs } from "../../utils/dayjs";
|
||||||
import { requiredString } from "../../utils/form-validation";
|
import { requiredString } from "../../utils/form-validation";
|
||||||
import { trpc } from "../../utils/trpc";
|
import { trpc } from "../../utils/trpc";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
|
@ -25,6 +25,7 @@ interface CommentForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Discussion: React.VoidFunctionComponent = () => {
|
const Discussion: React.VoidFunctionComponent = () => {
|
||||||
|
const { dayjs } = useDayjs();
|
||||||
const queryClient = trpc.useContext();
|
const queryClient = trpc.useContext();
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { poll } = usePoll();
|
const { poll } = usePoll();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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 * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/button";
|
import { Button } from "@/components/button";
|
||||||
|
@ -19,6 +20,7 @@ const ErrorPage: React.VoidFunctionComponent<ComponentProps> = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation("errors");
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex h-full max-w-full items-center justify-center bg-gray-50 px-4 py-8 lg:w-[1024px]">
|
<div className="mx-auto flex h-full max-w-full items-center justify-center bg-gray-50 px-4 py-8 lg:w-[1024px]">
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -28,16 +30,16 @@ const ErrorPage: React.VoidFunctionComponent<ComponentProps> = ({
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Icon className="mb-4 inline-block w-24 text-slate-400" />
|
<Icon className="mb-4 inline-block w-24 text-slate-400" />
|
||||||
<div className="text-primary-500 mb-2 text-3xl font-bold ">
|
<div className="mb-2 text-3xl font-bold text-primary-500 ">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
<div className="flex justify-center space-x-3">
|
<div className="flex justify-center space-x-3">
|
||||||
<Link href="/" passHref={true}>
|
<Link href="/" passHref={true}>
|
||||||
<a className="btn-default">Go to home</a>
|
<a className="btn-default">{t("goToHome")}</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Button icon={<Chat />} onClick={showCrispChat}>
|
<Button icon={<Chat />} onClick={showCrispChat}>
|
||||||
Start chat
|
{t("startChat")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { usePlausible } from "next-plausible";
|
import { usePlausible } from "next-plausible";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { usePreferences } from "@/components/preferences/use-preferences";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
expectTimeOption,
|
expectTimeOption,
|
||||||
getDateProps,
|
getDateProps,
|
||||||
removeAllOptionsForDay,
|
removeAllOptionsForDay,
|
||||||
} from "../../../../utils/date-time-utils";
|
} from "../../../../utils/date-time-utils";
|
||||||
|
import { useDayjs } from "../../../../utils/dayjs";
|
||||||
import { Button } from "../../../button";
|
import { Button } from "../../../button";
|
||||||
import CompactButton from "../../../compact-button";
|
import CompactButton from "../../../compact-button";
|
||||||
import DateCard from "../../../date-card";
|
import DateCard from "../../../date-card";
|
||||||
|
@ -38,6 +36,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
||||||
duration,
|
duration,
|
||||||
onChangeDuration,
|
onChangeDuration,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { dayjs, weekStartsOn } = useDayjs();
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const isTimedEvent = options.some((option) => option.type === "timeSlot");
|
const isTimedEvent = options.some((option) => option.type === "timeSlot");
|
||||||
|
|
||||||
|
@ -76,8 +75,6 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
||||||
);
|
);
|
||||||
}, [optionsByDay]);
|
}, [optionsByDay]);
|
||||||
|
|
||||||
const { weekStartsOn } = usePreferences();
|
|
||||||
|
|
||||||
const datepicker = useHeadlessDatePicker({
|
const datepicker = useHeadlessDatePicker({
|
||||||
selection: datepickerSelection,
|
selection: datepickerSelection,
|
||||||
onNavigationChange: onNavigate,
|
onNavigationChange: onNavigate,
|
||||||
|
|
|
@ -7,11 +7,11 @@ import {
|
||||||
} from "@floating-ui/react-dom-interactions";
|
} from "@floating-ui/react-dom-interactions";
|
||||||
import { Listbox } from "@headlessui/react";
|
import { Listbox } from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { stopPropagation } from "@/utils/stop-propagation";
|
import { stopPropagation } from "@/utils/stop-propagation";
|
||||||
|
|
||||||
|
import { useDayjs } from "../../../../utils/dayjs";
|
||||||
import ChevronDown from "../../../icons/chevron-down.svg";
|
import ChevronDown from "../../../icons/chevron-down.svg";
|
||||||
import { styleMenuItem } from "../../../menu-styles";
|
import { styleMenuItem } from "../../../menu-styles";
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ const TimePicker: React.VoidFunctionComponent<TimePickerProps> = ({
|
||||||
className,
|
className,
|
||||||
startFrom,
|
startFrom,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { dayjs } = useDayjs();
|
||||||
const { reference, floating, x, y, strategy, refs } = useFloating({
|
const { reference, floating, x, y, strategy, refs } = useFloating({
|
||||||
strategy: "fixed",
|
strategy: "fixed",
|
||||||
middleware: [
|
middleware: [
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Calendar } from "react-big-calendar";
|
import { Calendar } from "react-big-calendar";
|
||||||
import { useMount } from "react-use";
|
import { useMount } from "react-use";
|
||||||
|
|
||||||
import { getDuration } from "../../../utils/date-time-utils";
|
import { getDuration } from "../../../utils/date-time-utils";
|
||||||
import { usePreferences } from "../../preferences/use-preferences";
|
import { useDayjs } from "../../../utils/dayjs";
|
||||||
import DateNavigationToolbar from "./date-navigation-toolbar";
|
import DateNavigationToolbar from "./date-navigation-toolbar";
|
||||||
import dayjsLocalizer from "./dayjs-localizer";
|
import dayjsLocalizer from "./dayjs-localizer";
|
||||||
import { DateTimeOption, DateTimePickerProps } from "./types";
|
import { DateTimeOption, DateTimePickerProps } from "./types";
|
||||||
import { formatDateWithoutTime, formatDateWithoutTz } from "./utils";
|
import { formatDateWithoutTime, formatDateWithoutTz } from "./utils";
|
||||||
|
|
||||||
const localizer = dayjsLocalizer(dayjs);
|
|
||||||
|
|
||||||
const WeekCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
const WeekCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
||||||
title,
|
title,
|
||||||
options,
|
options,
|
||||||
|
@ -23,8 +20,9 @@ const WeekCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
||||||
onChangeDuration,
|
onChangeDuration,
|
||||||
}) => {
|
}) => {
|
||||||
const [scrollToTime, setScrollToTime] = React.useState<Date>();
|
const [scrollToTime, setScrollToTime] = React.useState<Date>();
|
||||||
|
const { dayjs, timeFormat } = useDayjs();
|
||||||
|
const localizer = React.useMemo(() => dayjsLocalizer(dayjs), [dayjs]);
|
||||||
|
|
||||||
const { timeFormat } = usePreferences();
|
|
||||||
useMount(() => {
|
useMount(() => {
|
||||||
// Bit of a hack to force rbc to scroll to the right time when we close/open a modal
|
// Bit of a hack to force rbc to scroll to the right time when we close/open a modal
|
||||||
setScrollToTime(dayjs(date).add(-60, "minutes").toDate());
|
setScrollToTime(dayjs(date).add(-60, "minutes").toDate());
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { useDayjs } from "../utils/dayjs";
|
||||||
|
|
||||||
interface DayProps {
|
interface DayProps {
|
||||||
date: Date;
|
date: Date;
|
||||||
day: string;
|
day: string;
|
||||||
|
@ -33,6 +34,7 @@ export const useHeadlessDatePicker = (
|
||||||
selection: Date[];
|
selection: Date[];
|
||||||
toggle: (date: Date) => void;
|
toggle: (date: Date) => void;
|
||||||
} => {
|
} => {
|
||||||
|
const { dayjs } = useDayjs();
|
||||||
const [localSelection, setSelection] = React.useState<Date[]>([]);
|
const [localSelection, setSelection] = React.useState<Date[]>([]);
|
||||||
const selection = options?.selection ?? localSelection;
|
const selection = options?.selection ?? localSelection;
|
||||||
const [localNavigationDate, setNavigationDate] = React.useState(today);
|
const [localNavigationDate, setNavigationDate] = React.useState(today);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import Link from "next/link";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { DayjsProvider } from "../../utils/dayjs";
|
||||||
import { UserAvatarProvider } from "../poll/user-avatar";
|
import { UserAvatarProvider } from "../poll/user-avatar";
|
||||||
import PollDemo from "./poll-demo";
|
import PollDemo from "./poll-demo";
|
||||||
import ScribbleArrow from "./scribble-arrow.svg";
|
import ScribbleArrow from "./scribble-arrow.svg";
|
||||||
|
@ -43,6 +44,7 @@ const Hero: React.VoidFunctionComponent = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="pointer-events-none mt-24 hidden h-[380px] select-none items-end justify-center md:flex lg:mt-8 lg:ml-24">
|
<div className="pointer-events-none mt-24 hidden h-[380px] select-none items-end justify-center md:flex lg:mt-8 lg:ml-24">
|
||||||
<UserAvatarProvider seed="mock" names={names}>
|
<UserAvatarProvider seed="mock" names={names}>
|
||||||
|
<DayjsProvider>
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute z-20 h-full rounded-2xl border-4 border-primary-500 bg-primary-200/10 shadow-md"
|
className="absolute z-20 h-full rounded-2xl border-4 border-primary-500 bg-primary-200/10 shadow-md"
|
||||||
|
@ -73,6 +75,7 @@ const Hero: React.VoidFunctionComponent = () => {
|
||||||
<PollDemo />
|
<PollDemo />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
</DayjsProvider>
|
||||||
</UserAvatarProvider>
|
</UserAvatarProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { useDayjs } from "../../utils/dayjs";
|
||||||
import { ParticipantRowView } from "../poll/desktop-poll/participant-row";
|
import { ParticipantRowView } from "../poll/desktop-poll/participant-row";
|
||||||
import { ScoreSummary } from "../poll/score-summary";
|
import { ScoreSummary } from "../poll/score-summary";
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ const options = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"];
|
||||||
const PollDemo: React.VoidFunctionComponent = () => {
|
const PollDemo: React.VoidFunctionComponent = () => {
|
||||||
const { t } = useTranslation("homepage");
|
const { t } = useTranslation("homepage");
|
||||||
|
|
||||||
|
const { dayjs } = useDayjs();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="rounded-lg bg-white py-1 shadow-huge"
|
className="rounded-lg bg-white py-1 shadow-huge"
|
||||||
|
|
|
@ -12,9 +12,9 @@ import {
|
||||||
} from "@/utils/date-time-utils";
|
} from "@/utils/date-time-utils";
|
||||||
import { GetPollApiResponse } from "@/utils/trpc/types";
|
import { GetPollApiResponse } from "@/utils/trpc/types";
|
||||||
|
|
||||||
|
import { useDayjs } from "../utils/dayjs";
|
||||||
import ErrorPage from "./error-page";
|
import ErrorPage from "./error-page";
|
||||||
import { useParticipants } from "./participants-provider";
|
import { useParticipants } from "./participants-provider";
|
||||||
import { usePreferences } from "./preferences/use-preferences";
|
|
||||||
import { useSession } from "./session";
|
import { useSession } from "./session";
|
||||||
import { useRequiredContext } from "./use-required-context";
|
import { useRequiredContext } from "./use-required-context";
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ export const PollContextProvider: React.VoidFunctionComponent<{
|
||||||
[participants],
|
[participants],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { timeFormat } = usePreferences();
|
const { timeFormat } = useDayjs();
|
||||||
|
|
||||||
const contextValue = React.useMemo<PollContextValue>(() => {
|
const contextValue = React.useMemo<PollContextValue>(() => {
|
||||||
const highScore = poll.options.reduce((acc, curr) => {
|
const highScore = poll.options.reduce((acc, curr) => {
|
||||||
|
|
|
@ -27,7 +27,6 @@ import VoteIcon from "./poll/vote-icon";
|
||||||
import { usePoll } from "./poll-context";
|
import { usePoll } from "./poll-context";
|
||||||
import { useSession } from "./session";
|
import { useSession } from "./session";
|
||||||
import Sharing from "./sharing";
|
import Sharing from "./sharing";
|
||||||
import StandardLayout from "./standard-layout";
|
|
||||||
|
|
||||||
const Discussion = React.lazy(() => import("@/components/discussion"));
|
const Discussion = React.lazy(() => import("@/components/discussion"));
|
||||||
|
|
||||||
|
@ -120,7 +119,6 @@ const PollPage: NextPage = () => {
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<UserAvatarProvider seed={poll.id} names={names}>
|
<UserAvatarProvider seed={poll.id} names={names}>
|
||||||
<StandardLayout>
|
|
||||||
<div className="relative max-w-full py-4 md:px-4">
|
<div className="relative max-w-full py-4 md:px-4">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{poll.title}</title>
|
<title>{poll.title}</title>
|
||||||
|
@ -240,9 +238,7 @@ const PollPage: NextPage = () => {
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<span className="inline-flex items-center space-x-1">
|
<span className="inline-flex items-center space-x-1">
|
||||||
<VoteIcon type="yes" />
|
<VoteIcon type="yes" />
|
||||||
<span className="text-xs text-slate-500">
|
<span className="text-xs text-slate-500">{t("yes")}</span>
|
||||||
{t("yes")}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center space-x-1">
|
<span className="inline-flex items-center space-x-1">
|
||||||
<VoteIcon type="ifNeedBe" />
|
<VoteIcon type="ifNeedBe" />
|
||||||
|
@ -252,9 +248,7 @@ const PollPage: NextPage = () => {
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center space-x-1">
|
<span className="inline-flex items-center space-x-1">
|
||||||
<VoteIcon type="no" />
|
<VoteIcon type="no" />
|
||||||
<span className="text-xs text-slate-500">
|
<span className="text-xs text-slate-500">{t("no")}</span>
|
||||||
{t("no")}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -265,14 +259,11 @@ const PollPage: NextPage = () => {
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<React.Suspense
|
<React.Suspense fallback={<div className="p-4">{t("loading")}</div>}>
|
||||||
fallback={<div className="p-4">{t("loading")}</div>}
|
|
||||||
>
|
|
||||||
<Discussion />
|
<Discussion />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</StandardLayout>
|
|
||||||
</UserAvatarProvider>
|
</UserAvatarProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
export const LanguageSelect: React.VoidFunctionComponent<{
|
export const LanguageSelect: React.VoidFunctionComponent<{
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (language: string) => void;
|
onChange?: (language: string) => void;
|
||||||
}> = ({ className, onChange }) => {
|
}> = ({ className, onChange }) => {
|
||||||
const { t } = useTranslation("common");
|
const { t, i18n } = useTranslation("common");
|
||||||
const router = useRouter();
|
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
className={clsx("input", className)}
|
className={clsx("input", className)}
|
||||||
defaultValue={router.locale}
|
defaultValue={i18n.language}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
Cookies.set("NEXT_LOCALE", e.target.value, {
|
Cookies.set("NEXT_LOCALE", e.target.value, {
|
||||||
expires: 365,
|
expires: 365,
|
||||||
|
@ -21,6 +19,7 @@ export const LanguageSelect: React.VoidFunctionComponent<{
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="en">{t("english")}</option>
|
<option value="en">{t("english")}</option>
|
||||||
|
<option value="es">{t("spanish")}</option>
|
||||||
<option value="fr">{t("french")}</option>
|
<option value="fr">{t("french")}</option>
|
||||||
<option value="de">{t("german")}</option>
|
<option value="de">{t("german")}</option>
|
||||||
<option value="sv">{t("swedish")}</option>
|
<option value="sv">{t("swedish")}</option>
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
import { usePoll } from "@/components/poll-context";
|
import { usePoll } from "@/components/poll-context";
|
||||||
|
|
||||||
|
import { useDayjs } from "../../../utils/dayjs";
|
||||||
import { useParticipants } from "../../participants-provider";
|
import { useParticipants } from "../../participants-provider";
|
||||||
|
|
||||||
export const useCsvExporter = () => {
|
export const useCsvExporter = () => {
|
||||||
|
const { dayjs } = useDayjs();
|
||||||
const { poll, options } = usePoll();
|
const { poll, options } = usePoll();
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { participants } = useParticipants();
|
const { participants } = useParticipants();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { useDayjs } from "../../utils/dayjs";
|
||||||
import Badge from "../badge";
|
import Badge from "../badge";
|
||||||
import { usePoll } from "../poll-context";
|
import { usePoll } from "../poll-context";
|
||||||
import Tooltip from "../tooltip";
|
import Tooltip from "../tooltip";
|
||||||
|
@ -9,7 +9,7 @@ import Tooltip from "../tooltip";
|
||||||
const PollSubheader: React.VoidFunctionComponent = () => {
|
const PollSubheader: React.VoidFunctionComponent = () => {
|
||||||
const { poll } = usePoll();
|
const { poll } = usePoll();
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
|
const { dayjs } = useDayjs();
|
||||||
return (
|
return (
|
||||||
<div className="text-slate-500/75 lg:text-lg">
|
<div className="text-slate-500/75 lg:text-lg">
|
||||||
<div className="md:inline">
|
<div className="md:inline">
|
||||||
|
|
|
@ -4,15 +4,14 @@ import { useTranslation } from "next-i18next";
|
||||||
import { usePlausible } from "next-plausible";
|
import { usePlausible } from "next-plausible";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { useDayjs } from "../utils/dayjs";
|
||||||
import { LanguageSelect } from "./poll/language-selector";
|
import { LanguageSelect } from "./poll/language-selector";
|
||||||
import { usePreferences } from "./preferences/use-preferences";
|
|
||||||
|
|
||||||
const Preferences: React.VoidFunctionComponent = () => {
|
const Preferences: React.VoidFunctionComponent = () => {
|
||||||
const { t } = useTranslation(["app", "common"]);
|
const { t } = useTranslation(["app", "common"]);
|
||||||
|
|
||||||
const { weekStartsOn, setWeekStartsOn, timeFormat, setTimeFormat } =
|
const { weekStartsOn, setWeekStartsOn, timeFormat, setTimeFormat } =
|
||||||
usePreferences();
|
useDayjs();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const plausible = usePlausible();
|
const plausible = usePlausible();
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import de from "dayjs/locale/de";
|
|
||||||
import en from "dayjs/locale/en";
|
|
||||||
import fr from "dayjs/locale/fr";
|
|
||||||
import sv from "dayjs/locale/sv";
|
|
||||||
import duration from "dayjs/plugin/duration";
|
|
||||||
import isBetween from "dayjs/plugin/isBetween";
|
|
||||||
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
|
|
||||||
import localeData from "dayjs/plugin/localeData";
|
|
||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
|
||||||
import minMax from "dayjs/plugin/minMax";
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
import timezone from "dayjs/plugin/timezone";
|
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import * as React from "react";
|
|
||||||
import { useLocalStorage } from "react-use";
|
|
||||||
|
|
||||||
type TimeFormat = "12h" | "24h";
|
|
||||||
type StartOfWeek = "monday" | "sunday";
|
|
||||||
|
|
||||||
const dayJsLocales = {
|
|
||||||
de,
|
|
||||||
en,
|
|
||||||
fr,
|
|
||||||
sv,
|
|
||||||
};
|
|
||||||
|
|
||||||
dayjs.extend(localizedFormat);
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
dayjs.extend(localeData);
|
|
||||||
dayjs.extend(isSameOrBefore);
|
|
||||||
dayjs.extend(isBetween);
|
|
||||||
dayjs.extend(minMax);
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
dayjs.extend(duration);
|
|
||||||
|
|
||||||
export const PreferencesContext =
|
|
||||||
React.createContext<{
|
|
||||||
weekStartsOn: StartOfWeek;
|
|
||||||
timeFormat: TimeFormat;
|
|
||||||
setWeekStartsOn: React.Dispatch<
|
|
||||||
React.SetStateAction<StartOfWeek | undefined>
|
|
||||||
>;
|
|
||||||
setTimeFormat: React.Dispatch<React.SetStateAction<TimeFormat | undefined>>;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
PreferencesContext.displayName = "PreferencesContext";
|
|
||||||
|
|
||||||
const PreferencesProvider: React.VoidFunctionComponent<{
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}> = ({ children }) => {
|
|
||||||
const [weekStartsOn = "monday", setWeekStartsOn] =
|
|
||||||
useLocalStorage<StartOfWeek>("rallly-week-starts-on");
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const userLocale = dayJsLocales[router.locale ?? "en"];
|
|
||||||
const [timeFormat = "12h", setTimeFormat] =
|
|
||||||
useLocalStorage<TimeFormat>("rallly-time-format");
|
|
||||||
|
|
||||||
dayjs.locale({
|
|
||||||
...userLocale,
|
|
||||||
weekStart: weekStartsOn === "monday" ? 1 : 0,
|
|
||||||
formats: { LT: timeFormat === "12h" ? "h:mm A" : "HH:mm" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const contextValue = React.useMemo(
|
|
||||||
() => ({
|
|
||||||
weekStartsOn,
|
|
||||||
timeFormat,
|
|
||||||
setWeekStartsOn,
|
|
||||||
setTimeFormat,
|
|
||||||
}),
|
|
||||||
[setTimeFormat, setWeekStartsOn, timeFormat, weekStartsOn],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PreferencesContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</PreferencesContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PreferencesProvider;
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { useRequiredContext } from "../use-required-context";
|
|
||||||
import { PreferencesContext } from "./preferences-provider";
|
|
||||||
|
|
||||||
export const usePreferences = () => {
|
|
||||||
return useRequiredContext(PreferencesContext);
|
|
||||||
};
|
|
|
@ -1,4 +1,3 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
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";
|
||||||
|
@ -8,6 +7,7 @@ import Calendar from "@/components/icons/calendar.svg";
|
||||||
import Pencil from "@/components/icons/pencil.svg";
|
import Pencil from "@/components/icons/pencil.svg";
|
||||||
import User from "@/components/icons/user.svg";
|
import User from "@/components/icons/user.svg";
|
||||||
|
|
||||||
|
import { useDayjs } from "../utils/dayjs";
|
||||||
import { trpc } from "../utils/trpc";
|
import { trpc } from "../utils/trpc";
|
||||||
import { EmptyState } from "./empty-state";
|
import { EmptyState } from "./empty-state";
|
||||||
import LoginForm from "./login-form";
|
import LoginForm from "./login-form";
|
||||||
|
@ -16,6 +16,7 @@ import { useSession } from "./session";
|
||||||
|
|
||||||
export const Profile: React.VoidFunctionComponent = () => {
|
export const Profile: React.VoidFunctionComponent = () => {
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
|
const { dayjs } = useDayjs();
|
||||||
|
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { data: userPolls } = trpc.useQuery(["user.getPolls"]);
|
const { data: userPolls } = trpc.useQuery(["user.getPolls"]);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import User from "@/components/icons/user.svg";
|
||||||
import UserCircle from "@/components/icons/user-circle.svg";
|
import UserCircle from "@/components/icons/user-circle.svg";
|
||||||
import Logo from "~/public/logo.svg";
|
import Logo from "~/public/logo.svg";
|
||||||
|
|
||||||
|
import { DayjsProvider } from "../utils/dayjs";
|
||||||
import Dropdown, { DropdownItem, DropdownProps } from "./dropdown";
|
import Dropdown, { DropdownItem, DropdownProps } from "./dropdown";
|
||||||
import Adjustments from "./icons/adjustments.svg";
|
import Adjustments from "./icons/adjustments.svg";
|
||||||
import Cash from "./icons/cash.svg";
|
import Cash from "./icons/cash.svg";
|
||||||
|
@ -23,7 +24,7 @@ import Support from "./icons/support.svg";
|
||||||
import Twitter from "./icons/twitter.svg";
|
import Twitter from "./icons/twitter.svg";
|
||||||
import LoginForm from "./login-form";
|
import LoginForm from "./login-form";
|
||||||
import { useModal } from "./modal";
|
import { useModal } from "./modal";
|
||||||
import { useModalContext } from "./modal/modal-provider";
|
import ModalProvider, { useModalContext } from "./modal/modal-provider";
|
||||||
import Popover from "./popover";
|
import Popover from "./popover";
|
||||||
import Preferences from "./preferences";
|
import Preferences from "./preferences";
|
||||||
import { useSession } from "./session";
|
import { useSession } from "./session";
|
||||||
|
@ -244,6 +245,8 @@ const StandardLayout: React.VoidFunctionComponent<{
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ModalProvider>
|
||||||
|
<DayjsProvider>
|
||||||
<div
|
<div
|
||||||
className="relative flex min-h-full flex-col bg-gray-50 lg:flex-row"
|
className="relative flex min-h-full flex-col bg-gray-50 lg:flex-row"
|
||||||
{...rest}
|
{...rest}
|
||||||
|
@ -276,7 +279,9 @@ const StandardLayout: React.VoidFunctionComponent<{
|
||||||
trigger={
|
trigger={
|
||||||
<button className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
|
<button className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
|
||||||
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
|
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
|
||||||
<span className="grow text-left">{t("app:preferences")}</span>
|
<span className="grow text-left">
|
||||||
|
{t("app:preferences")}
|
||||||
|
</span>
|
||||||
<DotsVertical className="h-4 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100" />
|
<DotsVertical className="h-4 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@ -303,7 +308,11 @@ const StandardLayout: React.VoidFunctionComponent<{
|
||||||
<motion.button
|
<motion.button
|
||||||
initial={{ x: -20, opacity: 0 }}
|
initial={{ x: -20, opacity: 0 }}
|
||||||
animate={{ x: 0, opacity: 1 }}
|
animate={{ x: 0, opacity: 1 }}
|
||||||
exit={{ x: -20, opacity: 0, transition: { duration: 0.2 } }}
|
exit={{
|
||||||
|
x: -20,
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.2 },
|
||||||
|
}}
|
||||||
className="group w-full rounded-lg p-2 px-3 text-left text-inherit transition-colors hover:bg-slate-500/10 active:bg-slate-500/20"
|
className="group w-full rounded-lg p-2 px-3 text-left text-inherit transition-colors hover:bg-slate-500/10 active:bg-slate-500/20"
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center space-x-3">
|
<div className="flex w-full items-center space-x-3">
|
||||||
|
@ -392,6 +401,8 @@ const StandardLayout: React.VoidFunctionComponent<{
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</DayjsProvider>
|
||||||
|
</ModalProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
const supportedLocales = ["en", "de", "fr", "sv"];
|
const supportedLocales = ["en", "es", "de", "fr", "sv"];
|
||||||
|
|
||||||
export function middleware({ headers, cookies, nextUrl }: NextRequest) {
|
export function middleware({ headers, cookies, nextUrl }: NextRequest) {
|
||||||
const locale =
|
const locale =
|
||||||
|
|
|
@ -1,16 +1,28 @@
|
||||||
|
import { GetStaticProps, NextPage } from "next";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import ErrorPage from "@/components/error-page";
|
import ErrorPage from "@/components/error-page";
|
||||||
import DocumentSearch from "@/components/icons/document-search.svg";
|
import DocumentSearch from "@/components/icons/document-search.svg";
|
||||||
|
|
||||||
const Custom404: React.VoidFunctionComponent = () => {
|
const Custom404: NextPage = () => {
|
||||||
|
const { t } = useTranslation("errors");
|
||||||
return (
|
return (
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
icon={DocumentSearch}
|
icon={DocumentSearch}
|
||||||
title="404 not found"
|
title={t("notFoundTitle")}
|
||||||
description="We couldn't find the page you're looking for."
|
description={t("notFoundDescription")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getStaticProps: GetStaticProps = async ({ locale = "en" }) => {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
...(await serverSideTranslations(locale, ["errors"])),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default Custom404;
|
export default Custom404;
|
||||||
|
|
|
@ -14,8 +14,6 @@ import { MutationCache } from "react-query";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import Maintenance from "@/components/maintenance";
|
import Maintenance from "@/components/maintenance";
|
||||||
import ModalProvider from "@/components/modal/modal-provider";
|
|
||||||
import PreferencesProvider from "@/components/preferences/preferences-provider";
|
|
||||||
|
|
||||||
import { AppRouter } from "./api/trpc/[trpc]";
|
import { AppRouter } from "./api/trpc/[trpc]";
|
||||||
|
|
||||||
|
@ -35,16 +33,12 @@ const MyApp: NextPage<AppProps> = ({ Component, pageProps }) => {
|
||||||
selfHosted={true}
|
selfHosted={true}
|
||||||
enabled={!!process.env.PLAUSIBLE_DOMAIN}
|
enabled={!!process.env.PLAUSIBLE_DOMAIN}
|
||||||
>
|
>
|
||||||
<PreferencesProvider>
|
|
||||||
<Head>
|
<Head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</Head>
|
</Head>
|
||||||
<CrispChat />
|
<CrispChat />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<ModalProvider>
|
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ModalProvider>
|
|
||||||
</PreferencesProvider>
|
|
||||||
</PlausibleProvider>
|
</PlausibleProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { PollContextProvider } from "@/components/poll-context";
|
||||||
import { withSession } from "@/components/session";
|
import { withSession } from "@/components/session";
|
||||||
|
|
||||||
import { ParticipantsProvider } from "../components/participants-provider";
|
import { ParticipantsProvider } from "../components/participants-provider";
|
||||||
|
import StandardLayout from "../components/standard-layout";
|
||||||
import { withSessionSsr } from "../utils/auth";
|
import { withSessionSsr } from "../utils/auth";
|
||||||
import { trpc } from "../utils/trpc";
|
import { trpc } from "../utils/trpc";
|
||||||
import { withPageTranslations } from "../utils/with-page-translations";
|
import { withPageTranslations } from "../utils/with-page-translations";
|
||||||
|
@ -34,11 +35,13 @@ const PollPageLoader: NextPage = () => {
|
||||||
|
|
||||||
if (poll) {
|
if (poll) {
|
||||||
return (
|
return (
|
||||||
|
<StandardLayout>
|
||||||
<ParticipantsProvider pollId={poll.id}>
|
<ParticipantsProvider pollId={poll.id}>
|
||||||
<PollContextProvider poll={poll} urlId={urlId} admin={admin}>
|
<PollContextProvider poll={poll} urlId={urlId} admin={admin}>
|
||||||
<PollPage />
|
<PollPage />
|
||||||
</PollContextProvider>
|
</PollContextProvider>
|
||||||
</ParticipantsProvider>
|
</ParticipantsProvider>
|
||||||
|
</StandardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +53,7 @@ const PollPageLoader: NextPage = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
||||||
withPageTranslations(["common", "app"]),
|
withPageTranslations(["common", "app", "errors"]),
|
||||||
);
|
);
|
||||||
|
|
||||||
export default withSession(PollPageLoader);
|
export default withSession(PollPageLoader);
|
||||||
|
|
123
src/utils/dayjs.tsx
Normal file
123
src/utils/dayjs.tsx
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import duration from "dayjs/plugin/duration";
|
||||||
|
import isBetween from "dayjs/plugin/isBetween";
|
||||||
|
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
|
||||||
|
import localeData from "dayjs/plugin/localeData";
|
||||||
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
|
import minMax from "dayjs/plugin/minMax";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useAsync, useLocalStorage } from "react-use";
|
||||||
|
|
||||||
|
import { useRequiredContext } from "../components/use-required-context";
|
||||||
|
|
||||||
|
export type TimeFormat = "12h" | "24h";
|
||||||
|
export type StartOfWeek = "monday" | "sunday";
|
||||||
|
|
||||||
|
const dayjsLocales: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
weekStartsOn: StartOfWeek;
|
||||||
|
timeFormat: TimeFormat;
|
||||||
|
import: () => Promise<ILocale>;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
en: {
|
||||||
|
weekStartsOn: "monday",
|
||||||
|
timeFormat: "12h",
|
||||||
|
import: () => import("dayjs/locale/en"),
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
weekStartsOn: "monday",
|
||||||
|
timeFormat: "24h",
|
||||||
|
import: () => import("dayjs/locale/es"),
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
weekStartsOn: "monday",
|
||||||
|
timeFormat: "24h",
|
||||||
|
import: () => import("dayjs/locale/de"),
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
weekStartsOn: "monday",
|
||||||
|
timeFormat: "24h",
|
||||||
|
import: () => import("dayjs/locale/fr"),
|
||||||
|
},
|
||||||
|
sv: {
|
||||||
|
weekStartsOn: "monday",
|
||||||
|
timeFormat: "24h",
|
||||||
|
import: () => import("dayjs/locale/sv"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
dayjs.extend(localizedFormat);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.extend(localeData);
|
||||||
|
dayjs.extend(isSameOrBefore);
|
||||||
|
dayjs.extend(isBetween);
|
||||||
|
dayjs.extend(minMax);
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
dayjs.extend(duration);
|
||||||
|
|
||||||
|
const DayjsContext =
|
||||||
|
React.createContext<{
|
||||||
|
dayjs: (date?: dayjs.ConfigType) => dayjs.Dayjs;
|
||||||
|
weekStartsOn: StartOfWeek;
|
||||||
|
timeFormat: TimeFormat;
|
||||||
|
setWeekStartsOn: React.Dispatch<
|
||||||
|
React.SetStateAction<StartOfWeek | undefined>
|
||||||
|
>;
|
||||||
|
setTimeFormat: React.Dispatch<React.SetStateAction<TimeFormat | undefined>>;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
export const useDayjs = () => {
|
||||||
|
return useRequiredContext(DayjsContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DayjsProvider: React.VoidFunctionComponent<{
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}> = ({ children }) => {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
// Using language instead of router.locale because when transitioning from homepage to
|
||||||
|
// the app via <Link locale={false}> it will be set to "en" instead of the current locale.
|
||||||
|
const locale = i18n.language;
|
||||||
|
|
||||||
|
const [weekStartsOn = dayjsLocales[locale].weekStartsOn, , setWeekStartsOn] =
|
||||||
|
useLocalStorage<StartOfWeek>("rallly-week-starts-on");
|
||||||
|
|
||||||
|
const [timeFormat = dayjsLocales[locale].timeFormat, setTimeFormat] =
|
||||||
|
useLocalStorage<TimeFormat>("rallly-time-format");
|
||||||
|
|
||||||
|
const { value: dayjsLocale } = useAsync(async () => {
|
||||||
|
return await dayjsLocales[locale ?? "en"].import();
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
|
if (dayjsLocale) {
|
||||||
|
dayjs.locale({
|
||||||
|
...dayjsLocale,
|
||||||
|
weekStart: weekStartsOn ? (weekStartsOn === "monday" ? 1 : 0) : undefined,
|
||||||
|
formats: {
|
||||||
|
...dayjsLocale.formats,
|
||||||
|
LT: timeFormat === "12h" ? "h:mm A" : "H:mm",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DayjsContext.Provider
|
||||||
|
value={{
|
||||||
|
dayjs,
|
||||||
|
weekStartsOn: weekStartsOn ?? dayjsLocales[locale].weekStartsOn,
|
||||||
|
timeFormat: timeFormat ?? dayjsLocales[locale].timeFormat,
|
||||||
|
setWeekStartsOn,
|
||||||
|
setTimeFormat,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DayjsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue