Translations (#225)

This commit is contained in:
Luke Vella 2022-07-17 17:11:56 +01:00 committed by GitHub
parent 9c61d34f24
commit bfb14b0e02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 342 additions and 275 deletions

View file

@ -1,6 +1,7 @@
[![Actions Status](https://github.com/lukevella/rallly/workflows/ci/badge.svg)](https://github.com/lukevella/rallly/actions) [![Actions Status](https://github.com/lukevella/rallly/workflows/ci/badge.svg)](https://github.com/lukevella/rallly/actions)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-orange.svg)](https://www.gnu.org/licenses/agpl-3.0) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-orange.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E) [![Discord](https://img.shields.io/badge/-Join%20Chat-7289DA?logo=discord&logoColor=white)](https://discord.gg/m5UFXavc2C)
[![Donate](https://img.shields.io/badge/-Donate%20with%20Paypal-white?logo=paypal)](https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E)
![hero](./docs/images/hero-image.png) ![hero](./docs/images/hero-image.png)
@ -104,7 +105,6 @@ Big thanks to these folks for sponsoring the project!
<a href="https://github.com/Daedalus3" target="_blank"><img src="https://avatars.githubusercontent.com/u/5649239?v=4" width="32" height="32" /></a>&nbsp; <a href="https://github.com/Daedalus3" target="_blank"><img src="https://avatars.githubusercontent.com/u/5649239?v=4" width="32" height="32" /></a>&nbsp;
<a href="https://github.com/iamericfletcher" target="_blank"><img src="https://avatars.githubusercontent.com/u/64165327?v=4" width="32" height="32" /></a>&nbsp; <a href="https://github.com/iamericfletcher" target="_blank"><img src="https://avatars.githubusercontent.com/u/64165327?v=4" width="32" height="32" /></a>&nbsp;
And thanks to these companies for providing their services to host and run [rallly.co](https://rallly.co). And thanks to these companies for providing their services to host and run [rallly.co](https://rallly.co).
<a href="https://vercel.com/?utm_source=rallly&utm_campaign=oss"><img src="public/vercel-logotype-dark.svg" alt="Powered by Vercel" height="30" /></a> <a href="https://vercel.com/?utm_source=rallly&utm_campaign=oss"><img src="public/vercel-logotype-dark.svg" alt="Powered by Vercel" height="30" /></a>

View file

@ -2,12 +2,10 @@ import "react-i18next";
import app from "~/public/locales/en/app.json"; import app from "~/public/locales/en/app.json";
import homepage from "~/public/locales/en/homepage.json"; import homepage from "~/public/locales/en/homepage.json";
import support from "~/public/locales/en/support.json";
declare module "next-i18next" { declare module "next-i18next" {
interface Resources { interface Resources {
homepage: typeof homepage; homepage: typeof homepage;
support: typeof support;
app: typeof app; app: typeof app;
} }
} }

View file

@ -1,61 +1,119 @@
{ {
"next": "Continue",
"back": "Back",
"newPoll": "New poll",
"eventDetails": "Poll Details",
"options": "Options",
"finish": "Finish",
"title": "Title",
"name": "Name",
"email": "Email",
"titlePlaceholder": "Monthly Meetup",
"locationPlaceholder": "Joe's Coffee Shop",
"descriptionPlaceholder": "Hey everyone, please choose the dates that work for you!",
"namePlaceholder": "Jessie Smith",
"emailPlaceholder": "jessie.smith@email.com",
"createPoll": "Create poll",
"location": "Location",
"description": "Description",
"stepSummary": "Step {{current}} of {{total}}",
"calendarHelp": "You can't create a poll without any options. Add at least one option to continue.",
"errorCreate": "Uh oh! There was a problem creating your poll. The error has been logged and we'll try to fix it.",
"share": "Share",
"shareDescription": "Give this link to your <b>participants</b> to allow them to vote on your poll.",
"requiredNameError": "Name is required",
"remove": "Remove",
"change": "Change",
"save": "Save",
"cancel": "Cancel",
"vote": "Vote",
"voteCount": "{{count}} vote",
"voteCount_other": "{{count}} votes",
"participantCount": "{{count}} participant",
"participantCount_other": "{{count}} participants",
"createdBy": "by <b>{{name}}</b>",
"timeZone": "Time Zone:",
"creatingDemo": "Creating demo poll…",
"ok": "Ok",
"loading": "Loading…",
"loadingParticipants": "Loading participants…",
"admin": "Admin",
"participant": "Participant",
"unverifiedMessage": "An email has been sent to <b>{{email}}</b> with a link to verify the email address.",
"notificationsOnDescription": "An email will be sent to <b>{{email}}</b> when there is activity on this poll.",
"deletingOptionsWarning": "You are deleting options that participants have voted for. Their votes will be also be deleted.",
"timeAndDate": "Time & date",
"weekStartsOn": "Week starts on:",
"timeFormat": "Time format:",
"monday": "Monday",
"sunday": "Sunday",
"12h": "12-hour", "12h": "12-hour",
"24h": "24-hour", "24h": "24-hour",
"yes": "Yes", "admin": "Admin",
"no": "No",
"ifNeedBe": "If need be",
"areYouSure": "Are you sure?", "areYouSure": "Are you sure?",
"deletePollDescription": "All data related to this poll will be deleted. To confirm, please type <s>“{{confirmText}}”</s> in to the input below:", "back": "Back",
"blog": "Blog",
"calendarHelp": "You can't create a poll without any options. Add at least one option to continue.",
"calendarHelpTitle": "Forget something?",
"cancel": "Cancel",
"comment": "Comment",
"applyToAllDates": "Apply to all dates",
"commentPlaceholder": "Leave a comment on this poll (visible to everyone)",
"deleteDate": "Delete date",
"addTimeOption": "Add time option",
"comments": "Comments",
"continue": "Continue",
"copied": "Copied",
"copyLink": "Copy link",
"createdBy": "by <b>{{name}}</b>",
"createPoll": "Create poll",
"creatingDemo": "Creating demo poll…",
"delete": "Delete",
"deleteComment": "Delete comment",
"deletedPoll": "Deleted poll",
"deletedPollInfo": "this poll doesn't exist anymore.",
"deletePoll": "Delete poll", "deletePoll": "Delete poll",
"deletePollDescription": "All data related to this poll will be deleted. To confirm, please type <s>“{{confirmText}}”</s> in to the input below:",
"deletingOptionsWarning": "You are deleting options that participants have voted for. Their votes will be also be deleted.",
"demoPollNotice": "Demo polls are automatically deleted after 1 day", "demoPollNotice": "Demo polls are automatically deleted after 1 day",
"description": "Description",
"descriptionPlaceholder": "Hey everyone, please choose the dates that work for you!",
"discussions": "Discussions",
"donate": "Donate",
"editDetails": "Edit details",
"editOptions": "Edit options",
"email": "Email",
"emailPlaceholder": "jessie.smith@email.com",
"endingGuestSessionNotice": "Once a guest session ends it cannot be resumed. You will not be able to edit any votes or comments you've made with this session.",
"endSession": "End session",
"errorCreate": "Uh oh! There was a problem creating your poll. The error has been logged and we'll try to fix it.",
"exportToCsv": "Export to CSV",
"finish": "Finish",
"forgetMe": "Forget me",
"goToAdmin": "Go to Admin",
"guest": "Guest",
"guestSessionNotice": "You are using a guest session. This allows us to recognize you if you come back later so you can edit your votes.",
"guestSessionReadMore": "Read more about guest sessions.",
"hide": "Hide",
"ifNeedBe": "If need be",
"linkHasExpired": "Your link has expired or is no longer valid",
"loading": "Loading…",
"loadingParticipants": "Loading participants…",
"location": "Location",
"locationPlaceholder": "Joe's Coffee Shop",
"lockPoll": "Lock poll",
"login": "Login",
"logout": "Logout",
"menu": "Menu",
"mixedOptionsDescription": "You can't have both time and date options in the same poll. Which would you like to keep?",
"mixedOptionsKeepDates": "Keep date options",
"mixedOptionsKeepTimes": "Keep time options",
"mixedOptionsTitle": "Wait a minute… 🤔",
"monday": "Monday",
"monthView": "Month view",
"name": "Name",
"namePlaceholder": "Jessie Smith",
"newPoll": "New poll",
"next": "Next",
"nextMonth": "Next month",
"no": "No",
"noDatesSelected": "No dates selected",
"notificationsDisabled": "Notifications have been disabled",
"notificationsOff": "Notifications are off",
"notificationsOn": "Notifications are on",
"notificationsOnDescription": "An email will be sent to <b>{{email}}</b> when there is activity on this poll.",
"notificationsVerifyEmail": "You need to verify your email to turn on notifications",
"ok": "Ok",
"options": "Options",
"participant": "Participant",
"participantCount_other": "{{count}} participants",
"participantCount": "{{count}} participant",
"pollHasBeenLocked": "This poll has been locked",
"pollHasBeenVerified": "Your poll has been verified",
"pollOwnerNotice": "Hey {{name}}, looks like you are the owner of this poll.",
"pollsEmpty": "No polls created",
"possibleAnswers": "Possible answers",
"preferences": "Preferences",
"previousMonth": "Previous month",
"profileLogin": "Profile - Login",
"profileUser": "Profile - {{username}}",
"remove": "Remove",
"requiredNameError": "Name is required",
"save": "Save",
"share": "Share",
"shareDescription": "Give this link to your <b>participants</b> to allow them to vote on your poll.",
"shareLink": "Share via link",
"specifyTimes": "Specify times",
"specifyTimesDescription": "Include start and end times for each option",
"stepSummary": "Step {{current}} of {{total}}",
"sunday": "Sunday",
"support": "Support",
"timeAndDate": "Time & date",
"timeFormat": "Time format:",
"timeZone": "Time Zone:",
"title": "Title",
"titlePlaceholder": "Monthly Meetup",
"unlockPoll": "Unlock poll",
"unverifiedMessage": "An email has been sent to <b>{{email}}</b> with a link to verify the email address.",
"user": "User",
"vote": "Vote",
"voteCount_other": "{{count}} votes",
"voteCount": "{{count}} vote",
"weekStartsOn": "Week starts on",
"weekView": "Week view",
"yes": "Yes",
"yourDetails": "Your details", "yourDetails": "Your details",
"yourPolls": "Your polls" "yourPolls": "Your polls"
} }

View file

@ -1,5 +1,44 @@
{ {
"3Ls": "Yes—with 3 <e>L</e>s",
"adFree": "Ad-free",
"adFreeDescription": "You can give your ad-blocker a rest — You won't need it here.",
"blog": "Blog",
"comments": "Comments",
"commentsDescription": "Participants can comment on your poll and the comments will be visible to everyone.",
"discussions": "Discussions",
"features": "Features",
"featuresSubheading": "Scheduling, the smart way",
"follow": "Follow",
"footerCredit": "Self-funded and built by <a>@imlukevella</a>",
"getStarted": "Get started", "getStarted": "Get started",
"viewDemo": "Live demo", "heroSubText": "Find the right date without the back and forth",
"footerCredit": "Self-funded and built by <a>@imlukevella</a>" "heroText": "Schedule<br/><s>group meetings</s><br />with ease",
"links": "Links",
"liveDemo": "Live demo",
"metaDescription": "Create polls and vote to find the best day or time. A free alternative to Doodle.",
"metaTitle": "Rallly - Schedule group meetings",
"mobileFriendly": "Mobile friendly",
"mobileFriendlyDescription": "Works great on mobile devices so participants can respond to polls wherever they may be.",
"new": "New",
"noLoginRequired": "No login required",
"noLoginRequiredDescription": "You don't need to login to create or participate in a poll",
"notifications": "Notifications",
"notificationsDescription": "Keep track of who's responded. Get notified when participants vote or comment on your poll.",
"openSource": "Open-source",
"openSourceDescription": "The codebase is fully open-source and <a>available on GitHub</a>.",
"participant": "Participant",
"participantCount_other": "{{count}} participants",
"participantCount": "{{count}} participant",
"perfect": "Perfect!",
"poweredBy": "Powered by",
"principles": "Principles",
"principlesSubheading": "We're not like the others",
"privacyPolicy": "Privacy policy",
"selfHostable": "Self-hostable",
"selfHostableDescription": "Run it on your own server to take full control of your data",
"sponsorThisProject": "Sponsor this project",
"star": "Star",
"support": "support",
"timeSlots": "Time slots",
"timeSlotsDescription": "Set individual start and end times for each option in your poll. Times can be automatically adjusted to each participant's timezone or they can be set to ignore timezones completely."
} }

View file

@ -1,19 +0,0 @@
{
"supportContactTitle": "Contact",
"supportContactMessage": "For any questions not covered here or if you'd like to share your feedback, send an email to: <a>support@rallly.co</a> or:",
"chatWithSupport": "Start chat",
"isMyDataSafeQuestion": "Is my data safe?",
"isMyDataSafeAnswer": "Yes! We do everything we can to keep your data safe and make sure your privacy is respected. We do not use your data to make a profit and it is not shared with any third-parties. If you have any concerns or would like to request the removal of your data from our servers please send an email to <a>support@rallly.co</a>",
"selfHostQuestion": "How do I run Rallly on my own server?",
"selfHostAnswer": "Check out the repository <a>README</a> for instructions on how to get Rallly running on your machine.",
"canYouHelpMeSetUpRalllyQuestion": "Can you help me set up Rallly on my own server?",
"canYouHelpMeSetUpRalllyAnswer": "Unfortunately it's not feasible to offer individual support to everyone. If you're having trouble getting it running you can ask for help on <a>github discussions</a>.",
"canRalllyDoQuestion": "How do I do <em>x</em>? Can Rallly do <em>y</em>?",
"canRalllyDoAnswer": "The best place to ask these questions are on our <a>github discussions</a>. You may find it has been asked already and if not your question will serve as a guide for others in the future.",
"legacyPollsQuestion": "What happened to the polls created with the previous version of Rallly?",
"legacyPollsAnswer": "Legacy polls are stored in a different database but you can still access the poll with the same URL. The polls will be transferred over in to the new database when you first try to access it. If a poll has not been accessed for at least two months then it might no longer be available but you can contact <a>support@rallly.co</a> to attempt to recover it.",
"howDoIShareQuestion": "How do I share my poll with my participants?",
"howDoIShareAnswer": "To share your poll, click on the <b>Share</b> button next to the title of your poll and copy the participant link. You can share this link with your participants through your own channels such as email, whatsapp, facebook etc…",
"contributeQuestion": "How can I contribute?",
"contributeAnswer": "Rallly is 100% self-funded so the best way to contribute is to become a <a>sponsor</a>. This money will go towards paying for hosting and will support future development of this website."
}

View file

@ -216,7 +216,7 @@ const Page: NextPage<CreatePollPageProps> = ({
type="primary" type="primary"
> >
{currentStepIndex < steps.length - 1 {currentStepIndex < steps.length - 1
? t("next") ? t("continue")
: t("createPoll")} : t("createPoll")}
</Button> </Button>
</div> </div>

View file

@ -1,6 +1,7 @@
import clsx from "clsx"; import clsx from "clsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
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";
@ -25,6 +26,7 @@ interface CommentForm {
const Discussion: React.VoidFunctionComponent = () => { const Discussion: React.VoidFunctionComponent = () => {
const queryClient = trpc.useContext(); const queryClient = trpc.useContext();
const { t } = useTranslation("app");
const { poll } = usePoll(); const { poll } = usePoll();
const pollId = poll.id; const pollId = poll.id;
@ -82,7 +84,7 @@ const Discussion: React.VoidFunctionComponent = () => {
return ( return (
<div className="overflow-hidden border-t border-b shadow-sm md:rounded-lg md:border"> <div className="overflow-hidden border-t border-b shadow-sm md:rounded-lg md:border">
<div className="border-b bg-white px-4 py-2"> <div className="border-b bg-white px-4 py-2">
<div className="font-medium">Comments</div> <div className="font-medium">{t("comments")}</div>
</div> </div>
<div <div
className={clsx({ className={clsx({
@ -129,7 +131,7 @@ const Discussion: React.VoidFunctionComponent = () => {
> >
<DropdownItem <DropdownItem
icon={Trash} icon={Trash}
label="Delete comment" label={t("deleteComment")}
disabled={!canDelete} disabled={!canDelete}
onClick={() => { onClick={() => {
deleteComment.mutate({ deleteComment.mutate({
@ -158,7 +160,7 @@ const Discussion: React.VoidFunctionComponent = () => {
> >
<textarea <textarea
id="comment" id="comment"
placeholder="Thanks for the invite!" placeholder={t("commentPlaceholder")}
className="input w-full py-2 pl-3 pr-4" className="input w-full py-2 pl-3 pr-4"
{...register("content", { validate: requiredString })} {...register("content", { validate: requiredString })}
/> />
@ -175,7 +177,7 @@ const Discussion: React.VoidFunctionComponent = () => {
/> />
</div> </div>
<Button htmlType="submit" loading={formState.isSubmitting}> <Button htmlType="submit" loading={formState.isSubmitting}>
Comment {t("comment")}
</Button> </Button>
</div> </div>
</form> </form>

View file

@ -1,5 +1,6 @@
import clsx from "clsx"; import clsx from "clsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
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";
@ -37,6 +38,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
duration, duration,
onChangeDuration, onChangeDuration,
}) => { }) => {
const { t } = useTranslation("app");
const isTimedEvent = options.some((option) => option.type === "timeSlot"); const isTimedEvent = options.some((option) => option.type === "timeSlot");
const plausible = usePlausible(); const plausible = usePlausible();
@ -91,14 +93,14 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
<div className="mb-3 flex items-center justify-center space-x-4"> <div className="mb-3 flex items-center justify-center space-x-4">
<Button <Button
icon={<ChevronLeft />} icon={<ChevronLeft />}
title="Previous month" title={t("previousMonth")}
onClick={datepicker.prev} onClick={datepicker.prev}
/> />
<div className="grow text-center text-lg font-medium"> <div className="grow text-center text-lg font-medium">
{datepicker.label} {datepicker.label}
</div> </div>
<Button <Button
title="Next month" title={t("nextMonth")}
icon={<ChevronRight />} icon={<ChevronRight />}
onClick={datepicker.next} onClick={datepicker.next}
/> />
@ -190,9 +192,9 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
> >
<div className="flex items-center space-x-3 p-4"> <div className="flex items-center space-x-3 p-4">
<div className="grow"> <div className="grow">
<div className="font-medium">Specify times</div> <div className="font-medium">{t("specifyTimes")}</div>
<div className="text-sm text-slate-400"> <div className="text-sm text-slate-400">
Include start and end times for each option {t("specifyTimesDescription")}
</div> </div>
</div> </div>
<div> <div>
@ -338,7 +340,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
]); ]);
}} }}
> >
Add time option {t("addTimeOption")}
</Button> </Button>
<Dropdown <Dropdown
trigger={<CompactButton icon={DotsHorizontal} />} trigger={<CompactButton icon={DotsHorizontal} />}
@ -347,7 +349,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
<DropdownItem <DropdownItem
icon={Magic} icon={Magic}
disabled={datepicker.selection.length < 2} disabled={datepicker.selection.length < 2}
label="Apply to all dates" label={t("applyToAllDates")}
onClick={() => { onClick={() => {
plausible("Applied options to all dates"); plausible("Applied options to all dates");
const times = optionsForDay.map( const times = optionsForDay.map(
@ -384,7 +386,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
}} }}
/> />
<DropdownItem <DropdownItem
label="Delete date" label={t("deleteDate")}
icon={Trash} icon={Trash}
onClick={() => { onClick={() => {
onChange( onChange(
@ -431,7 +433,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
<div className="flex h-full items-center justify-center py-12"> <div className="flex h-full items-center justify-center py-12">
<div className="text-center font-medium text-gray-400"> <div className="text-center font-medium text-gray-400">
<Calendar className="mb-2 inline-block h-12 w-12" /> <Calendar className="mb-2 inline-block h-12 w-12" />
<div>No dates selected</div> <div>{t("noDatesSelected")}</div>
</div> </div>
</div> </div>
)} )}

View file

@ -52,18 +52,18 @@ const PollOptionsForm: React.VoidFunctionComponent<
const views = React.useMemo(() => { const views = React.useMemo(() => {
const res = [ const res = [
{ {
label: "Month view", label: t("monthView"),
value: "month", value: "month",
Component: MonthCalendar, Component: MonthCalendar,
}, },
{ {
label: "Week view", label: t("weekView"),
value: "week", value: "week",
Component: WeekCalendar, Component: WeekCalendar,
}, },
]; ];
return res; return res;
}, []); }, [t]);
const watchView = watch("view"); const watchView = watch("view");
@ -79,10 +79,9 @@ const PollOptionsForm: React.VoidFunctionComponent<
const datesOnly = watchOptions.every((option) => option.type === "date"); const datesOnly = watchOptions.every((option) => option.type === "date");
const [dateOrTimeRangeModal, openDateOrTimeRangeModal] = useModal({ const [dateOrTimeRangeModal, openDateOrTimeRangeModal] = useModal({
title: "Wait a minute… 🤔", title: t("mixedOptionsTitle"),
description: description: t("mixedOptionsDescription"),
"You can't have both time and date options in the same poll. Which would you like to keep?", okText: t("mixedOptionsKeepTimes"),
okText: "Keep time options",
onOk: () => { onOk: () => {
setValue( setValue(
"options", "options",
@ -92,7 +91,7 @@ const PollOptionsForm: React.VoidFunctionComponent<
setValue("timeZone", getBrowserTimeZone()); setValue("timeZone", getBrowserTimeZone());
} }
}, },
cancelText: "Keep date options", cancelText: t("mixedOptionsKeepDates"),
onCancel: () => { onCancel: () => {
setValue( setValue(
"options", "options",
@ -130,7 +129,7 @@ const PollOptionsForm: React.VoidFunctionComponent<
const [calendarHelpModal, openHelpModal] = useModal({ const [calendarHelpModal, openHelpModal] = useModal({
overlayClosable: true, overlayClosable: true,
title: "Forget something?", title: t("calendarHelpTitle"),
description: t("calendarHelp"), description: t("calendarHelp"),
okText: t("ok"), okText: t("ok"),
}); });
@ -172,7 +171,7 @@ const PollOptionsForm: React.VoidFunctionComponent<
}} }}
type="button" type="button"
> >
<Calendar className="mr-2 h-5 w-5" /> Month view <Calendar className="mr-2 h-5 w-5" /> {t("monthView")}
</button> </button>
<button <button
className={clsx({ className={clsx({
@ -183,7 +182,7 @@ const PollOptionsForm: React.VoidFunctionComponent<
setValue("view", "week"); setValue("view", "week");
}} }}
> >
<Table className="mr-2 h-5 w-5" /> Week view <Table className="mr-2 h-5 w-5" /> {t("weekView")}
</button> </button>
</div> </div>
</div> </div>
@ -191,7 +190,9 @@ const PollOptionsForm: React.VoidFunctionComponent<
<div className="relative w-full"> <div className="relative w-full">
<React.Suspense <React.Suspense
fallback={ fallback={
<FullPageLoader className="h-[400px]">Loading</FullPageLoader> <FullPageLoader className="h-[400px]">
{t("loading")}
</FullPageLoader>
} }
> >
<selectedView.Component <selectedView.Component

View file

@ -38,7 +38,7 @@ export const UserDetailsForm: React.VoidFunctionComponent<
style={{ width: 400 }} style={{ width: 400 }}
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
> >
<h2>Your details</h2> <h2>{t("yourDetails")}</h2>
<div className="formField"> <div className="formField">
<label className="text-slate-500" htmlFor="name"> <label className="text-slate-500" htmlFor="name">
{t("name")} {t("name")}

View file

@ -1,4 +1,5 @@
import Head from "next/head"; import Head from "next/head";
import { useTranslation } from "next-i18next";
import React from "react"; import React from "react";
import Bonus from "./home/bonus"; import Bonus from "./home/bonus";
@ -7,14 +8,12 @@ import Hero from "./home/hero";
import PageLayout from "./page-layout"; import PageLayout from "./page-layout";
const Home: React.VoidFunctionComponent = () => { const Home: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
return ( return (
<PageLayout> <PageLayout>
<Head> <Head>
<meta <meta name="description" content={t("metaDescription")} />
name="description" <title>{t("metaTitle")}</title>
content="Create polls and vote to find the best day or time. A free alternative to Doodle."
/>
<title>Rallly - Schedule group meetings</title>
</Head> </Head>
<Hero /> <Hero />
<Features /> <Features />

View file

@ -1,3 +1,4 @@
import { Trans, useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
import Code from "@/components/icons/code.svg"; import Code from "@/components/icons/code.svg";
@ -7,50 +8,52 @@ import Server from "@/components/icons/server.svg";
import Ban from "./ban-ads.svg"; import Ban from "./ban-ads.svg";
const Bonus: React.VoidFunctionComponent = () => { const Bonus: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
return ( return (
<div className="mx-auto max-w-7xl px-8 pt-8 pb-24"> <div className="mx-auto max-w-7xl px-8 pt-8 pb-24">
<h2 className="heading">Principles</h2> <h2 className="heading">{t("principles")}</h2>
<p className="subheading">We&apos;re not like the others</p> <p className="subheading">{t("principlesSubheading")}</p>
<div className="grid grid-cols-4 gap-16"> <div className="grid grid-cols-4 gap-16">
<div className="col-span-4 md:col-span-2 lg:col-span-1"> <div className="col-span-4 md:col-span-2 lg:col-span-1">
<div className="mb-4 text-gray-400"> <div className="mb-4 text-gray-400">
<CursorClick className="w-16" /> <CursorClick className="w-16" />
</div> </div>
<h3 className="heading-sm">No login required</h3> <h3 className="heading-sm">{t("noLoginRequired")}</h3>
<div className="text text-base leading-relaxed"> <div className="text text-base leading-relaxed">
We keep things simple and don&apos;t ask for more than what we need. {t("noLoginRequiredDescription")}
</div> </div>
</div> </div>
<div className="col-span-4 md:col-span-2 lg:col-span-1"> <div className="col-span-4 md:col-span-2 lg:col-span-1">
<div className="mb-4 text-gray-400"> <div className="mb-4 text-gray-400">
<Code className="w-16" /> <Code className="w-16" />
</div> </div>
<h3 className="heading-sm">Open-source</h3> <h3 className="heading-sm">{t("openSource")}</h3>
<div className="text text-base leading-relaxed"> <div className="text text-base leading-relaxed">
The codebase is fully open-source and{" "} <Trans
<a href="https://github.com/lukevella/rallly"> t={t}
available on github i18nKey={"openSourceDescription"}
</a> components={{
. a: <a href="https://github.com/lukevella/rallly" />,
}}
/>
</div> </div>
</div> </div>
<div className="col-span-4 md:col-span-2 lg:col-span-1"> <div className="col-span-4 md:col-span-2 lg:col-span-1">
<div className="mb-4 text-gray-400"> <div className="mb-4 text-gray-400">
<Server className="w-16" /> <Server className="w-16" />
</div> </div>
<h3 className="heading-sm">Self-hostable</h3> <h3 className="heading-sm">{t("selfHostable")}</h3>
<div className="text text-base leading-relaxed"> <div className="text text-base leading-relaxed">
Run it on your own server to get full control of your data. {t("selfHostableDescription")}
</div> </div>
</div> </div>
<div className="col-span-4 md:col-span-2 lg:col-span-1"> <div className="col-span-4 md:col-span-2 lg:col-span-1">
<div className="mb-4 text-gray-400"> <div className="mb-4 text-gray-400">
<Ban className="w-16" /> <Ban className="w-16" />
</div> </div>
<h3 className="heading-sm">Ad-free</h3> <h3 className="heading-sm">{t("adFree")}</h3>
<div className="text text-base leading-relaxed"> <div className="text text-base leading-relaxed">
You can give your ad-blocker a rest &ndash; You won&apos;t need it {t("adFreeDescription")}
here.
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,3 +1,4 @@
import { useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
import Bell from "@/components/icons/bell.svg"; import Bell from "@/components/icons/bell.svg";
@ -6,56 +7,44 @@ import Clock from "@/components/icons/clock.svg";
import DeviceMobile from "@/components/icons/device-mobile.svg"; import DeviceMobile from "@/components/icons/device-mobile.svg";
const Features: React.VoidFunctionComponent = () => { const Features: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
return ( return (
<div className="mx-auto max-w-7xl py-16 px-8"> <div className="mx-auto max-w-7xl py-16 px-8">
<h2 className="heading">Features</h2> <h2 className="heading">{t("features")}</h2>
<p className="subheading">Everything you need to get the job done</p> <p className="subheading">{t("featuresSubheading")}</p>
<div className="grid grid-cols-2 gap-12"> <div className="grid grid-cols-2 gap-12">
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">
<div className="mb-4 inline-block rounded-2xl bg-green-100/50 p-3 text-green-400"> <div className="mb-4 inline-block rounded-2xl bg-green-100/50 p-3 text-green-400">
<Clock className="h-8 w-8" /> <Clock className="h-8 w-8" />
</div> </div>
<h3 className="heading-sm flex items-center"> <h3 className="heading-sm flex items-center">
Time slots {t("timeSlots")}
<span className="ml-2 rounded-full bg-green-500 px-2 py-1 text-sm font-normal text-white"> <span className="ml-2 rounded-full bg-green-500 px-2 py-1 text-sm font-normal text-white">
New {t("new")}
</span> </span>
</h3> </h3>
<p className="text"> <p className="text">{t("timeSlotsDescription")}</p>
If you need more granular options, Rallly lets you choose time slots
as options. If your participants are international, they can see
times in their own time zone.
</p>
</div> </div>
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">
<div className="mb-4 inline-block rounded-2xl bg-cyan-100/50 p-3 text-cyan-400"> <div className="mb-4 inline-block rounded-2xl bg-cyan-100/50 p-3 text-cyan-400">
<DeviceMobile className="h-8 w-8" /> <DeviceMobile className="h-8 w-8" />
</div> </div>
<h3 className="heading-sm">Mobile friendly design</h3> <h3 className="heading-sm">{t("mobileFriendly")}</h3>
<p className="text"> <p className="text">{t("mobileFriendlyDescription")}</p>
Rallly is optimized to look and work great on mobile devices so you
and your participants can use it on the go.
</p>
</div> </div>
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">
<div className="mb-4 inline-block rounded-2xl bg-rose-100/50 p-3 text-rose-400"> <div className="mb-4 inline-block rounded-2xl bg-rose-100/50 p-3 text-rose-400">
<Bell className="h-8 w-8" /> <Bell className="h-8 w-8" />
</div> </div>
<h3 className="heading-sm">Notifications</h3> <h3 className="heading-sm">{t("notifications")}</h3>
<p className="text"> <p className="text">{t("notificationsDescription")}</p>
Need help staying on top of things? Rallly can send you an email
whenever participants vote or comment on your poll.
</p>
</div> </div>
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">
<div className="mb-4 inline-block rounded-2xl bg-yellow-100/50 p-3 text-yellow-400"> <div className="mb-4 inline-block rounded-2xl bg-yellow-100/50 p-3 text-yellow-400">
<Chat className="h-8 w-8" /> <Chat className="h-8 w-8" />
</div> </div>
<h3 className="heading-sm">Comments</h3> <h3 className="heading-sm">{t("comments")}</h3>
<p className="text"> <p className="text">{t("commentsDescription")}</p>
Got a question or just have something to say? You and your
participants can comment on polls to start a discussion.
</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import Link from "next/link"; import Link from "next/link";
import { useTranslation } from "next-i18next"; import { Trans, useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
import { UserAvatarProvider } from "../poll/user-avatar"; import { UserAvatarProvider } from "../poll/user-avatar";
@ -15,15 +15,16 @@ const Hero: React.VoidFunctionComponent = () => {
<div className="mx-auto max-w-7xl items-end p-8 lg:flex lg:justify-between"> <div className="mx-auto max-w-7xl items-end p-8 lg:flex lg:justify-between">
<div className="my-8 text-center lg:text-left"> <div className="my-8 text-center lg:text-left">
<h1 className="text-4xl font-bold sm:text-5xl"> <h1 className="text-4xl font-bold sm:text-5xl">
Schedule <Trans
<br /> t={t}
<span className="text-primary-500">group&nbsp;meetings</span> i18nKey="heroText"
<br /> components={{
with ease br: <br />,
s: <span className="whitespace-nowrap text-primary-500" />,
}}
/>
</h1> </h1>
<div className="mb-12 text-xl text-gray-400"> <div className="mb-12 text-xl text-gray-400">{t("heroSubText")}</div>
Find the right date without the back and&nbsp;forth.
</div>
<div className="space-x-3"> <div className="space-x-3">
<Link href="/new"> <Link href="/new">
<a className="rounded-lg bg-primary-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-primary-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 focus:ring-primary-200 active:bg-primary-600/90"> <a className="rounded-lg bg-primary-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-primary-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 focus:ring-primary-200 active:bg-primary-600/90">
@ -35,7 +36,7 @@ const Hero: React.VoidFunctionComponent = () => {
className="rounded-lg bg-slate-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-slate-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 focus:ring-primary-200 active:bg-slate-600/90" className="rounded-lg bg-slate-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-slate-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 focus:ring-primary-200 active:bg-slate-600/90"
rel="nofollow" rel="nofollow"
> >
{t("viewDemo")} {t("liveDemo")}
</a> </a>
</Link> </Link>
</div> </div>
@ -60,7 +61,7 @@ const Hero: React.VoidFunctionComponent = () => {
animate={{ opacity: 1, translateY: 0 }} animate={{ opacity: 1, translateY: 0 }}
transition={{ type: "spring", delay: 2 }} transition={{ type: "spring", delay: 2 }}
> >
Perfect! 🤩 {t("perfect")} 🤩
<ScribbleArrow className="absolute -right-8 top-3 text-slate-400" /> <ScribbleArrow className="absolute -right-8 top-3 text-slate-400" />
</motion.div> </motion.div>
<motion.div <motion.div

View file

@ -33,7 +33,7 @@ const participants = [
const options = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"]; 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("app"); const { t } = useTranslation("homepage");
return ( return (
<div <div

View file

@ -1,30 +0,0 @@
import Link from "next/link";
import { useTranslation } from "next-i18next";
import * as React from "react";
const Stats: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
return (
<div className="py-16">
<h2 className="heading text-center">Stats</h2>
<p className="subheading text-center">100,000+ polls created</p>
<div className="flex justify-center space-x-3">
<Link href="/new">
<a className="bg-primary-500 hover:bg-primary-500/90 focus:ring-primary-200 active:bg-primary-600/90 rounded-lg px-5 py-3 font-semibold text-white shadow-sm transition-all hover:text-white hover:no-underline hover:shadow-md focus:ring-2">
{t("getStarted")}
</a>
</Link>
<Link href="/demo">
<a
className="focus:ring-primary-200 rounded-lg bg-slate-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-slate-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 active:bg-slate-600/90"
rel="nofollow"
>
{t("viewDemo")}
</a>
</Link>
</div>
</div>
);
};
export default Stats;

View file

@ -3,6 +3,7 @@ import dynamic from "next/dynamic";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
import { createBreakpoint } from "react-use"; import { createBreakpoint } from "react-use";
@ -28,7 +29,7 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
<Link href="/"> <Link href="/">
<a <a
className={clsx( className={clsx(
"hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2", "text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2",
{ {
"pointer-events-none font-bold text-gray-600": "pointer-events-none font-bold text-gray-600":
pathname === "/home", pathname === "/home",
@ -41,7 +42,7 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
<Link href="https://blog.rallly.co"> <Link href="https://blog.rallly.co">
<a <a
className={clsx( className={clsx(
"hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2", "text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2",
)} )}
> >
Blog Blog
@ -49,12 +50,12 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
</Link> </Link>
<a <a
href="https://support.rallly.co" href="https://support.rallly.co"
className="hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2" className="text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2"
> >
Support Support
</a> </a>
<Link href="https://github.com/lukevella/rallly"> <Link href="https://github.com/lukevella/rallly">
<a className="hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2"> <a className="text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2">
<Github className="w-6" /> <Github className="w-6" />
</a> </a>
</Link> </Link>
@ -66,6 +67,7 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
children, children,
}) => { }) => {
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();
const { t } = useTranslation("homepage");
return ( return (
<div className="bg-pattern min-h-full overflow-x-hidden"> <div className="bg-pattern min-h-full overflow-x-hidden">
<Head> <Head>
@ -76,11 +78,11 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
<div className="relative inline-block"> <div className="relative inline-block">
<Link href="/"> <Link href="/">
<a> <a>
<Logo className="text-primary-500 w-40" alt="Rallly" /> <Logo className="w-40 text-primary-500" alt="Rallly" />
</a> </a>
</Link> </Link>
<span className="absolute -bottom-6 right-0 text-sm text-slate-400 transition-colors"> <span className="absolute -bottom-6 right-0 text-sm text-slate-400 transition-colors">
Yes&mdash;with 3 <em>L</em>s <Trans t={t} i18nKey="3Ls" components={{ e: <em /> }} />
</span> </span>
</div> </div>
</div> </div>
@ -89,7 +91,7 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
<Popover <Popover
placement="left-start" placement="left-start"
trigger={ trigger={
<button className="hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2"> <button className="text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2">
<DotsVertical className="w-5" /> <DotsVertical className="w-5" />
</button> </button>
} }

View file

@ -34,32 +34,32 @@ const Footer: React.VoidFunctionComponent = () => {
aria-label="Star lukevella/rallly on GitHub" aria-label="Star lukevella/rallly on GitHub"
data-show-count={true} data-show-count={true}
> >
Star {t("star")}
</GitHubButton> </GitHubButton>
<GitHubButton <GitHubButton
href="https://github.com/sponsors/lukevella" href="https://github.com/sponsors/lukevella"
data-icon="octicon-heart" data-icon="octicon-heart"
aria-label="Sponsor @lukevella on GitHub" aria-label="Sponsor @lukevella on GitHub"
> >
Sponsor this project {t("sponsorThisProject")}
</GitHubButton> </GitHubButton>
</div> </div>
</div> </div>
<div className="col-span-6 md:col-span-2"> <div className="col-span-6 md:col-span-2">
<div className="mb-4 font-medium">Links</div> <div className="mb-4 font-medium">{t("links")}</div>
<ul> <ul>
<li> <li>
<a <a
className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline" className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"
href="https://github.com/lukevella/rallly/discussions" href="https://github.com/lukevella/rallly/discussions"
> >
Forum {t("discussions")}
</a> </a>
</li> </li>
<li> <li>
<Link href="https://blog.rallly.co"> <Link href="https://blog.rallly.co">
<a className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"> <a className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline">
Blog {t("blog")}
</a> </a>
</Link> </Link>
</li> </li>
@ -68,20 +68,20 @@ const Footer: React.VoidFunctionComponent = () => {
href="https://support.rallly.co" href="https://support.rallly.co"
className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline" className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"
> >
Support {t("support")}
</a> </a>
</li> </li>
<li> <li>
<Link href="/privacy-policy"> <Link href="/privacy-policy">
<a className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"> <a className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline">
Privacy Policy {t("privacyPolicy")}
</a> </a>
</Link> </Link>
</li> </li>
</ul> </ul>
</div> </div>
<div className="col-span-6 md:col-span-2"> <div className="col-span-6 md:col-span-2">
<div className="mb-4 font-medium">Follow</div> <div className="mb-4 font-medium">{t("follow")}</div>
<ul> <ul>
<li> <li>
<a <a
@ -107,7 +107,7 @@ const Footer: React.VoidFunctionComponent = () => {
className="inline-block text-white" className="inline-block text-white"
> >
<span className="mb-1 inline-block w-full text-right text-xs italic text-gray-400"> <span className="mb-1 inline-block w-full text-right text-xs italic text-gray-400">
Powered by {t("poweredBy")}
</span> </span>
<Vercel className="w-24" /> <Vercel className="w-24" />
</a> </a>

View file

@ -1,5 +1,6 @@
import { Participant, Vote, VoteType } from "@prisma/client"; import { Participant, Vote, VoteType } from "@prisma/client";
import { keyBy } from "lodash"; import { keyBy } from "lodash";
import { useTranslation } from "next-i18next";
import React from "react"; import React from "react";
import Trash from "@/components/icons/trash.svg"; import Trash from "@/components/icons/trash.svg";
@ -57,6 +58,7 @@ export const PollContextProvider: React.VoidFunctionComponent<{
admin: boolean; admin: boolean;
children?: React.ReactNode; children?: React.ReactNode;
}> = ({ poll, urlId, admin, children }) => { }> = ({ poll, urlId, admin, children }) => {
const { t } = useTranslation("app");
const { participants } = useParticipants(); const { participants } = useParticipants();
const [isDeleted, setDeleted] = React.useState(false); const [isDeleted, setDeleted] = React.useState(false);
const { user } = useSession(); const { user } = useSession();
@ -176,8 +178,8 @@ export const PollContextProvider: React.VoidFunctionComponent<{
return ( return (
<ErrorPage <ErrorPage
icon={Trash} icon={Trash}
title="Deleted poll" title={t("deletedPoll")}
description="This poll doesn't exist anymore." description={t("deletedPollInfo")}
/> />
); );
} }

View file

@ -52,7 +52,7 @@ const PollPage: NextPage = () => {
const verifyEmail = trpc.useMutation(["polls.verification.verify"], { const verifyEmail = trpc.useMutation(["polls.verification.verify"], {
onSuccess: () => { onSuccess: () => {
toast.success("Your poll has been verified"); toast.success(t("pollHasBeenVerified"));
queryClient.setQueryData(["polls.get", { urlId, admin }], { queryClient.setQueryData(["polls.get", { urlId, admin }], {
...poll, ...poll,
verified: true, verified: true,
@ -61,7 +61,7 @@ const PollPage: NextPage = () => {
plausible("Verified email"); plausible("Verified email");
}, },
onError: () => { onError: () => {
toast.error("Your link has expired or is no longer valid"); toast.error(t("linkHasExpired"));
}, },
onSettled: () => { onSettled: () => {
router.replace(`/admin/${router.query.urlId}`, undefined, { router.replace(`/admin/${router.query.urlId}`, undefined, {
@ -83,7 +83,7 @@ const PollPage: NextPage = () => {
{ urlId: urlId, notifications: false }, { urlId: urlId, notifications: false },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Notifications have been disabled"); toast.success(t("notificationsDisabled"));
plausible("Unsubscribed from notifications"); plausible("Unsubscribed from notifications");
}, },
}, },
@ -92,7 +92,7 @@ const PollPage: NextPage = () => {
shallow: true, shallow: true,
}); });
} }
}, [plausible, urlId, router, updatePollMutation]); }, [plausible, urlId, router, updatePollMutation, t]);
const checkIfWideScreen = () => window.innerWidth > 640; const checkIfWideScreen = () => window.innerWidth > 640;
@ -146,7 +146,7 @@ const PollPage: NextPage = () => {
setSharingVisible((value) => !value); setSharingVisible((value) => !value);
}} }}
> >
Share {t("share")}
</Button> </Button>
</div> </div>
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
@ -196,11 +196,10 @@ const PollPage: NextPage = () => {
{!poll.admin && poll.adminUrlId ? ( {!poll.admin && poll.adminUrlId ? (
<div className="mb-4 items-center justify-between rounded-lg px-4 md:flex md:space-x-4 md:border md:p-2 md:pl-4"> <div className="mb-4 items-center justify-between rounded-lg px-4 md:flex md:space-x-4 md:border md:p-2 md:pl-4">
<div className="mb-4 font-medium md:mb-0"> <div className="mb-4 font-medium md:mb-0">
Hey {poll.user.name}, looks like you are the owner of this {t("pollOwnerNotice", { name: poll.user.name })}
poll.
</div> </div>
<a href={`/admin/${poll.adminUrlId}`} className="btn-default"> <a href={`/admin/${poll.adminUrlId}`} className="btn-default">
Go to admin &rarr; {t("goToAdmin")} &rarr;
</a> </a>
</div> </div>
) : null} ) : null}
@ -210,7 +209,7 @@ const PollPage: NextPage = () => {
<LockClosed className="w-6" /> <LockClosed className="w-6" />
</div> </div>
<div> <div>
<div className="font-medium">This poll has been locked</div> <div className="font-medium">{t("pollHasBeenLocked")}</div>
</div> </div>
</div> </div>
) : null} ) : null}
@ -243,22 +242,26 @@ const PollPage: NextPage = () => {
) : null} ) : null}
<div> <div>
<div className="mb-2 text-sm text-slate-500"> <div className="mb-2 text-sm text-slate-500">
Possible answers {t("possibleAnswers")}
</div> </div>
<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">Yes</span> <span className="text-xs text-slate-500">
{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" />
<span className="text-xs text-slate-500"> <span className="text-xs text-slate-500">
If need be {t("ifNeedBe")}
</span> </span>
</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">No</span> <span className="text-xs text-slate-500">
{t("no")}
</span>
</span> </span>
</div> </div>
</div> </div>
@ -269,7 +272,9 @@ const PollPage: NextPage = () => {
</React.Suspense> </React.Suspense>
</div> </div>
<React.Suspense fallback={<div className="p-4">Loading</div>}> <React.Suspense
fallback={<div className="p-4">{t("loading")}</div>}
>
<Discussion /> <Discussion />
</React.Suspense> </React.Suspense>
</div> </div>

View file

@ -1,4 +1,5 @@
import clsx from "clsx"; import clsx from "clsx";
import { useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@ -27,6 +28,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
HTMLFormElement, HTMLFormElement,
ParticipantRowFormProps ParticipantRowFormProps
> = ({ defaultValues, onSubmit, className, onCancel }, ref) => { > = ({ defaultValues, onSubmit, className, onCancel }, ref) => {
const { t } = useTranslation("app");
const { const {
columnWidth, columnWidth,
scrollPosition, scrollPosition,
@ -168,7 +170,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
loading={isSubmitting} loading={isSubmitting}
data-testid="submitNewParticipant" data-testid="submitNewParticipant"
> >
Save {t("save")}
</Button> </Button>
) : null} ) : null}
{scrollPosition < maxScrollPosition ? ( {scrollPosition < maxScrollPosition ? (
@ -178,7 +180,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
goToNextPage(); goToNextPage();
}} }}
> >
Next &rarr; {t("next")} &rarr;
</Button> </Button>
) : null} ) : null}
{onCancel ? <CompactButton onClick={onCancel} icon={X} /> : null} {onCancel ? <CompactButton onClick={onCancel} icon={X} /> : null}

View file

@ -55,13 +55,13 @@ const ManagePoll: React.VoidFunctionComponent<{
openChangeOptionsModal, openChangeOptionsModal,
closeChangeOptionsModal, closeChangeOptionsModal,
] = useModal({ ] = useModal({
okText: "Save", okText: t("save"),
okButtonProps: { okButtonProps: {
form: "pollOptions", form: "pollOptions",
htmlType: "submit", htmlType: "submit",
loading: isUpdating, loading: isUpdating,
}, },
cancelText: "Cancel", cancelText: t("cancel"),
content: ( content: (
<React.Suspense fallback={null}> <React.Suspense fallback={null}>
<PollOptionsForm <PollOptionsForm
@ -116,7 +116,7 @@ const ManagePoll: React.VoidFunctionComponent<{
if (optionsToDeleteThatHaveVotes.length > 0) { if (optionsToDeleteThatHaveVotes.length > 0) {
modalContext.render({ modalContext.render({
title: "Are you sure?", title: t("areYouSure"),
description: ( description: (
<Trans <Trans
t={t} t={t}
@ -128,8 +128,8 @@ const ManagePoll: React.VoidFunctionComponent<{
okButtonProps: { okButtonProps: {
type: "danger", type: "danger",
}, },
okText: "Delete", okText: t("delete"),
cancelText: "Cancel", cancelText: t("cancel"),
}); });
} else { } else {
onOk(); onOk();
@ -145,13 +145,13 @@ const ManagePoll: React.VoidFunctionComponent<{
openChangePollDetailsModa, openChangePollDetailsModa,
closePollDetailsModal, closePollDetailsModal,
] = useModal({ ] = useModal({
okText: "Save changes", okText: t("save"),
okButtonProps: { okButtonProps: {
form: "updateDetails", form: "updateDetails",
loading: isUpdating, loading: isUpdating,
htmlType: "submit", htmlType: "submit",
}, },
cancelText: "Cancel", cancelText: t("cancel"),
content: ( content: (
<PollDetailsForm <PollDetailsForm
name="updateDetails" name="updateDetails"
@ -181,31 +181,35 @@ const ManagePoll: React.VoidFunctionComponent<{
> >
<DropdownItem <DropdownItem
icon={Pencil} icon={Pencil}
label="Edit details" label={t("editDetails")}
onClick={openChangePollDetailsModa} onClick={openChangePollDetailsModa}
/> />
<DropdownItem <DropdownItem
icon={Table} icon={Table}
label="Edit options" label={t("editOptions")}
onClick={handleChangeOptions} onClick={handleChangeOptions}
/> />
<DropdownItem icon={Save} label="Export to CSV" onClick={exportToCsv} /> <DropdownItem
icon={Save}
label={t("exportToCsv")}
onClick={exportToCsv}
/>
{poll.closed ? ( {poll.closed ? (
<DropdownItem <DropdownItem
icon={LockOpen} icon={LockOpen}
label="Unlock poll" label={t("unlockPoll")}
onClick={() => updatePollMutation({ urlId, closed: false })} onClick={() => updatePollMutation({ urlId, closed: false })}
/> />
) : ( ) : (
<DropdownItem <DropdownItem
icon={LockClosed} icon={LockClosed}
label="Lock poll" label={t("lockPoll")}
onClick={() => updatePollMutation({ urlId, closed: true })} onClick={() => updatePollMutation({ urlId, closed: true })}
/> />
)} )}
<DropdownItem <DropdownItem
icon={Trash} icon={Trash}
label="Delete poll" label={t("deletePoll")}
onClick={() => { onClick={() => {
modalContext.render({ modalContext.render({
overlayClosable: true, overlayClosable: true,

View file

@ -26,7 +26,7 @@ const NotificationsToggle: React.VoidFunctionComponent = () => {
poll.notifications ? ( poll.notifications ? (
<div> <div>
<div className="font-medium text-primary-300"> <div className="font-medium text-primary-300">
Notifications are on {t("notificationsOn")}
</div> </div>
<div className="max-w-sm"> <div className="max-w-sm">
<Trans <Trans
@ -44,10 +44,10 @@ const NotificationsToggle: React.VoidFunctionComponent = () => {
</div> </div>
</div> </div>
) : ( ) : (
"Notifications are off" t("notificationsOff")
) )
) : ( ) : (
"You need to verify your email to turn on notifications" t("notificationsVerifyEmail")
) )
} }
> >

View file

@ -22,7 +22,9 @@ const Preferences: React.VoidFunctionComponent = () => {
</div> </div>
<div className="grow"> <div className="grow">
<div className="mb-2"> <div className="mb-2">
<div className="mb-2 grow text-sm text-slate-500">Week starts on</div> <div className="mb-2 grow text-sm text-slate-500">
{t("weekStartsOn")}
</div>
<div> <div>
<div className="segment-button inline-flex"> <div className="segment-button inline-flex">
<button <button
@ -61,7 +63,9 @@ const Preferences: React.VoidFunctionComponent = () => {
</div> </div>
</div> </div>
<div className="mb-2"> <div className="mb-2">
<div className="mb-2 grow text-sm text-slate-500">Time format</div> <div className="mb-2 grow text-sm text-slate-500">
{t("timeFormat")}
</div>
<div className="segment-button inline-flex"> <div className="segment-button inline-flex">
<button <button
className={clsx({ className={clsx({

View file

@ -26,7 +26,7 @@ export const Profile: React.VoidFunctionComponent = () => {
return ( return (
<div className="card my-4 p-0"> <div className="card my-4 p-0">
<Head> <Head>
<title>Profile - Login</title> <title>{t("profileLogin")}</title>
</Head> </Head>
<LoginForm /> <LoginForm />
</div> </div>
@ -36,7 +36,11 @@ export const Profile: React.VoidFunctionComponent = () => {
return ( return (
<div className="mx-auto max-w-3xl py-4 lg:mx-0"> <div className="mx-auto max-w-3xl py-4 lg:mx-0">
<Head> <Head>
<title>Profile - {user.name}</title> <title>
{t("profileUser", {
username: user.name,
})}
</title>
</Head> </Head>
<div className="mb-4 flex items-center px-4"> <div className="mb-4 flex items-center px-4">
<div className="mr-4 inline-flex h-14 w-14 items-center justify-center rounded-lg bg-primary-50"> <div className="mr-4 inline-flex h-14 w-14 items-center justify-center rounded-lg bg-primary-50">
@ -50,7 +54,7 @@ export const Profile: React.VoidFunctionComponent = () => {
{user.shortName} {user.shortName}
</div> </div>
<div className="text-slate-500"> <div className="text-slate-500">
{user.isGuest ? "Guest" : "User"} {user.isGuest ? t("guest") : t("user")}
</div> </div>
</div> </div>
</div> </div>
@ -92,7 +96,7 @@ export const Profile: React.VoidFunctionComponent = () => {
</div> </div>
</div> </div>
) : ( ) : (
<EmptyState icon={Pencil} text="No polls created" /> <EmptyState icon={Pencil} text={t("pollsEmpty")} />
)} )}
</div> </div>
) : null} ) : null}

View file

@ -32,13 +32,13 @@ const Sharing: React.VoidFunctionComponent<SharingProps> = ({
<div className={clsx("card p-4", className)}> <div className={clsx("card p-4", className)}>
<div className="mb-1 flex items-center justify-between"> <div className="mb-1 flex items-center justify-between">
<div className="text-lg font-semibold text-slate-700"> <div className="text-lg font-semibold text-slate-700">
Share via link {t("shareLink")}
</div> </div>
<button <button
onClick={onHide} onClick={onHide}
className="h-8 items-center justify-center rounded-md px-3 text-slate-400 transition-colors hover:bg-slate-500/10 hover:text-slate-500 active:bg-slate-500/20" className="h-8 items-center justify-center rounded-md px-3 text-slate-400 transition-colors hover:bg-slate-500/10 hover:text-slate-500 active:bg-slate-500/20"
> >
Hide {t("hide")}
</button> </button>
</div> </div>
<div className="mb-4 text-slate-600"> <div className="mb-4 text-slate-600">
@ -71,7 +71,7 @@ const Sharing: React.VoidFunctionComponent<SharingProps> = ({
}} }}
className="md:absolute md:top-1/2 md:right-3 md:-translate-y-1/2" className="md:absolute md:top-1/2 md:right-3 md:-translate-y-1/2"
> >
{didCopy ? "Copied" : "Copy Link"} {didCopy ? t("copied") : t("copyLink")}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import clsx from "clsx"; import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link"; import Link from "next/link";
import { useTranslation } from "next-i18next";
import React from "react"; import React from "react";
import Menu from "@/components/icons/menu.svg"; import Menu from "@/components/icons/menu.svg";
@ -40,6 +41,7 @@ const MobileNavigation: React.VoidFunctionComponent<{
openLoginModal: () => void; openLoginModal: () => void;
}> = ({ openLoginModal }) => { }> = ({ openLoginModal }) => {
const { user } = useSession(); const { user } = useSession();
const { t } = useTranslation("app");
return ( return (
<div <div
className="fixed top-0 z-40 flex h-12 w-full shrink-0 items-center justify-between border-b bg-gray-50 className="fixed top-0 z-40 flex h-12 w-full shrink-0 items-center justify-between border-b bg-gray-50
@ -55,7 +57,7 @@ const MobileNavigation: React.VoidFunctionComponent<{
className="flex w-full cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300" className="flex w-full cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
> >
<Login className="h-5 opacity-75" /> <Login className="h-5 opacity-75" />
<span className="inline-block">Login</span> <span className="inline-block">{t("login")}</span>
</button> </button>
)} )}
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
@ -93,7 +95,7 @@ const MobileNavigation: React.VoidFunctionComponent<{
className="group flex items-center whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300" className="group flex items-center whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
> >
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500" /> <Adjustments className="h-5 opacity-75 group-hover:text-primary-500" />
<span className="ml-2 hidden sm:block">Preferences</span> <span className="ml-2 hidden sm:block">{t("preferences")}</span>
</button> </button>
} }
> >
@ -107,7 +109,7 @@ const MobileNavigation: React.VoidFunctionComponent<{
className="group flex items-center rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300" className="group flex items-center rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
> >
<Menu className="w-5 group-hover:text-primary-500" /> <Menu className="w-5 group-hover:text-primary-500" />
<span className="ml-2 hidden sm:block">Menu</span> <span className="ml-2 hidden sm:block">{t("menu")}</span>
</button> </button>
} }
> >
@ -121,12 +123,13 @@ const MobileNavigation: React.VoidFunctionComponent<{
const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({ const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
className, className,
}) => { }) => {
const { t } = useTranslation("app");
return ( return (
<div className={clsx("space-y-1", className)}> <div className={clsx("space-y-1", className)}>
<Link href="/new"> <Link href="/new">
<a className="flex cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 pr-4 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"> <a className="flex cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 pr-4 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300">
<Pencil className="h-5 opacity-75 " /> <Pencil className="h-5 opacity-75 " />
<span className="inline-block">New Poll</span> <span className="inline-block">{t("newPoll")}</span>
</a> </a>
</Link> </Link>
<a <a
@ -136,7 +139,7 @@ const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
rel="noreferrer" rel="noreferrer"
> >
<Support className="h-5 opacity-75" /> <Support className="h-5 opacity-75" />
<span className="inline-block">Support</span> <span className="inline-block">{t("support")}</span>
</a> </a>
</div> </div>
); );
@ -146,6 +149,7 @@ const UserDropdown: React.VoidFunctionComponent<
DropdownProps & { openLoginModal: () => void } DropdownProps & { openLoginModal: () => void }
> = ({ children, openLoginModal, ...forwardProps }) => { > = ({ children, openLoginModal, ...forwardProps }) => {
const { logout, user } = useSession(); const { logout, user } = useSession();
const { t } = useTranslation("app");
const modalContext = useModalContext(); const modalContext = useModalContext();
if (!user) { if (!user) {
return null; return null;
@ -175,17 +179,14 @@ const UserDropdown: React.VoidFunctionComponent<
</div> </div>
</div> </div>
</div> </div>
<p> <p>{t("guestSessionNotice")}</p>
You are using a guest session. This allows us to recognize
you if you come back later so you can edit your votes.
</p>
<div> <div>
<a <a
href="https://support.rallly.co/guest-sessions" href="https://support.rallly.co/guest-sessions"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
Read more about guest sessions. {t("guestSessionReadMore")}
</a> </a>
</div> </div>
</div> </div>
@ -204,20 +205,19 @@ const UserDropdown: React.VoidFunctionComponent<
) : null} ) : null}
<DropdownItem <DropdownItem
icon={Logout} icon={Logout}
label={user.isGuest ? "Forget me" : "Logout"} label={user.isGuest ? t("forgetMe") : t("logout")}
onClick={() => { onClick={() => {
if (user?.isGuest) { if (user?.isGuest) {
modalContext.render({ modalContext.render({
title: "Are you sure?", title: t("areYouSure"),
description: description: t("endingGuestSessionNotice"),
"Once a guest session ends it cannot be resumed. You will not be able to edit any votes or comments you've made with this session.",
onOk: logout, onOk: logout,
okButtonProps: { okButtonProps: {
type: "danger", type: "danger",
}, },
okText: "End session", okText: t("endSession"),
cancelText: "Cancel", cancelText: t("cancel"),
}); });
} else { } else {
logout(); logout();
@ -232,6 +232,7 @@ const StandardLayout: React.VoidFunctionComponent<{
children?: React.ReactNode; children?: React.ReactNode;
}> = ({ children, ...rest }) => { }> = ({ children, ...rest }) => {
const { user } = useSession(); const { user } = useSession();
const { t } = useTranslation("app");
const [loginModal, openLoginModal] = useModal({ const [loginModal, openLoginModal] = useModal({
footer: null, footer: null,
overlayClosable: true, overlayClosable: true,
@ -255,7 +256,7 @@ const StandardLayout: React.VoidFunctionComponent<{
<Link href="/new"> <Link href="/new">
<a className="group mb-1 flex 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"> <a className="group mb-1 flex 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">
<Pencil className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" /> <Pencil className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">New Poll</span> <span className="grow text-left">{t("newPoll")}</span>
</a> </a>
</Link> </Link>
<a <a
@ -265,14 +266,14 @@ const StandardLayout: React.VoidFunctionComponent<{
rel="noreferrer" rel="noreferrer"
> >
<Support className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" /> <Support className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">Support</span> <span className="grow text-left">{t("support")}</span>
</a> </a>
<Popover <Popover
placement="right-start" placement="right-start"
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">Preferences</span> <span className="grow text-left">{t("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>
} }
@ -285,7 +286,7 @@ const StandardLayout: React.VoidFunctionComponent<{
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" 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"
> >
<Login className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" /> <Login className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">Login</span> <span className="grow text-left">{t("login")}</span>
</button> </button>
)} )}
</div> </div>
@ -311,7 +312,7 @@ const StandardLayout: React.VoidFunctionComponent<{
{user.shortName} {user.shortName}
</div> </div>
<div className="truncate text-xs text-slate-500"> <div className="truncate text-xs text-slate-500">
{user.isGuest ? "Guest" : "User"} {user.isGuest ? t("guest") : t("user")}
</div> </div>
</div> </div>
<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" />
@ -343,16 +344,16 @@ const StandardLayout: React.VoidFunctionComponent<{
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline" className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
rel="noreferrer" rel="noreferrer"
> >
Support {t("support")}
</a> </a>
<Link href="https://github.com/lukevella/rallly/discussions"> <Link href="https://github.com/lukevella/rallly/discussions">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"> <a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
Discussions {t("discussions")}
</a> </a>
</Link> </Link>
<Link href="https://blog.rallly.co"> <Link href="https://blog.rallly.co">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"> <a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
Blog {t("blog")}
</a> </a>
</Link> </Link>
<div className="hidden text-slate-300 lg:block">&bull;</div> <div className="hidden text-slate-300 lg:block">&bull;</div>
@ -373,7 +374,7 @@ const StandardLayout: React.VoidFunctionComponent<{
<Link href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"> <Link href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E">
<a className="inline-flex h-8 items-center rounded-full bg-slate-100 pl-2 pr-3 text-sm text-slate-400 transition-colors hover:bg-primary-500 hover:text-white hover:no-underline focus:ring-2 focus:ring-primary-500 focus:ring-offset-1 active:bg-primary-600"> <a className="inline-flex h-8 items-center rounded-full bg-slate-100 pl-2 pr-3 text-sm text-slate-400 transition-colors hover:bg-primary-500 hover:text-white hover:no-underline focus:ring-2 focus:ring-primary-500 focus:ring-offset-1 active:bg-primary-600">
<Cash className="mr-1 inline-block w-5" /> <Cash className="mr-1 inline-block w-5" />
<span>Donate</span> <span>{t("donate")}</span>
</a> </a>
</Link> </Link>
</div> </div>

View file

@ -7,7 +7,7 @@ export const getStaticProps: GetStaticProps = async ({ locale = "en" }) => {
try { try {
return { return {
props: { props: {
...(await serverSideTranslations(locale, ["app", "homepage"])), ...(await serverSideTranslations(locale, ["homepage"])),
}, },
}; };
} catch { } catch {

View file

@ -23,7 +23,7 @@ test("should be able to vote and comment on a poll", async ({ page }) => {
page.locator("data-testid=participant-row >> nth=4").locator("text=You"), page.locator("data-testid=participant-row >> nth=4").locator("text=You"),
).toBeVisible(); ).toBeVisible();
await page.type( await page.type(
"[placeholder='Thanks for the invite!']", "[placeholder='Leave a comment on this poll (visible to everyone)']",
"This is a comment!", "This is a comment!",
); );
await page.type('[placeholder="Your name…"]', "Test user"); await page.type('[placeholder="Your name…"]', "Test user");