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)
[![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)
@ -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/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).
<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 homepage from "~/public/locales/en/homepage.json";
import support from "~/public/locales/en/support.json";
declare module "next-i18next" {
interface Resources {
homepage: typeof homepage;
support: typeof support;
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",
"24h": "24-hour",
"yes": "Yes",
"no": "No",
"ifNeedBe": "If need be",
"admin": "Admin",
"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",
"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",
"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",
"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",
"viewDemo": "Live demo",
"footerCredit": "Self-funded and built by <a>@imlukevella</a>"
"heroSubText": "Find the right date without the back and forth",
"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"
>
{currentStepIndex < steps.length - 1
? t("next")
? t("continue")
: t("createPoll")}
</Button>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import { useTranslation } from "next-i18next";
import * as React from "react";
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";
const Features: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
return (
<div className="mx-auto max-w-7xl py-16 px-8">
<h2 className="heading">Features</h2>
<p className="subheading">Everything you need to get the job done</p>
<h2 className="heading">{t("features")}</h2>
<p className="subheading">{t("featuresSubheading")}</p>
<div className="grid grid-cols-2 gap-12">
<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">
<Clock className="h-8 w-8" />
</div>
<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">
New
{t("new")}
</span>
</h3>
<p className="text">
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>
<p className="text">{t("timeSlotsDescription")}</p>
</div>
<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">
<DeviceMobile className="h-8 w-8" />
</div>
<h3 className="heading-sm">Mobile friendly design</h3>
<p className="text">
Rallly is optimized to look and work great on mobile devices so you
and your participants can use it on the go.
</p>
<h3 className="heading-sm">{t("mobileFriendly")}</h3>
<p className="text">{t("mobileFriendlyDescription")}</p>
</div>
<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">
<Bell className="h-8 w-8" />
</div>
<h3 className="heading-sm">Notifications</h3>
<p className="text">
Need help staying on top of things? Rallly can send you an email
whenever participants vote or comment on your poll.
</p>
<h3 className="heading-sm">{t("notifications")}</h3>
<p className="text">{t("notificationsDescription")}</p>
</div>
<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">
<Chat className="h-8 w-8" />
</div>
<h3 className="heading-sm">Comments</h3>
<p className="text">
Got a question or just have something to say? You and your
participants can comment on polls to start a discussion.
</p>
<h3 className="heading-sm">{t("comments")}</h3>
<p className="text">{t("commentsDescription")}</p>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
import { motion } from "framer-motion";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
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="my-8 text-center lg:text-left">
<h1 className="text-4xl font-bold sm:text-5xl">
Schedule
<br />
<span className="text-primary-500">group&nbsp;meetings</span>
<br />
with ease
<Trans
t={t}
i18nKey="heroText"
components={{
br: <br />,
s: <span className="whitespace-nowrap text-primary-500" />,
}}
/>
</h1>
<div className="mb-12 text-xl text-gray-400">
Find the right date without the back and&nbsp;forth.
</div>
<div className="mb-12 text-xl text-gray-400">{t("heroSubText")}</div>
<div className="space-x-3">
<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">
@ -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"
rel="nofollow"
>
{t("viewDemo")}
{t("liveDemo")}
</a>
</Link>
</div>
@ -60,7 +61,7 @@ const Hero: React.VoidFunctionComponent = () => {
animate={{ opacity: 1, translateY: 0 }}
transition={{ type: "spring", delay: 2 }}
>
Perfect! 🤩
{t("perfect")} 🤩
<ScribbleArrow className="absolute -right-8 top-3 text-slate-400" />
</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 PollDemo: React.VoidFunctionComponent = () => {
const { t } = useTranslation("app");
const { t } = useTranslation("homepage");
return (
<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 Link from "next/link";
import { useRouter } from "next/router";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import { createBreakpoint } from "react-use";
@ -28,7 +29,7 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
<Link href="/">
<a
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":
pathname === "/home",
@ -41,7 +42,7 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
<Link href="https://blog.rallly.co">
<a
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
@ -49,12 +50,12 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
</Link>
<a
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
</a>
<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" />
</a>
</Link>
@ -66,6 +67,7 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
children,
}) => {
const breakpoint = useBreakpoint();
const { t } = useTranslation("homepage");
return (
<div className="bg-pattern min-h-full overflow-x-hidden">
<Head>
@ -76,11 +78,11 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
<div className="relative inline-block">
<Link href="/">
<a>
<Logo className="text-primary-500 w-40" alt="Rallly" />
<Logo className="w-40 text-primary-500" alt="Rallly" />
</a>
</Link>
<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>
</div>
</div>
@ -89,7 +91,7 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
<Popover
placement="left-start"
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" />
</button>
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@ const NotificationsToggle: React.VoidFunctionComponent = () => {
poll.notifications ? (
<div>
<div className="font-medium text-primary-300">
Notifications are on
{t("notificationsOn")}
</div>
<div className="max-w-sm">
<Trans
@ -44,10 +44,10 @@ const NotificationsToggle: React.VoidFunctionComponent = () => {
</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 className="grow">
<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 className="segment-button inline-flex">
<button
@ -61,7 +63,9 @@ const Preferences: React.VoidFunctionComponent = () => {
</div>
</div>
<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">
<button
className={clsx({

View file

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

View file

@ -32,13 +32,13 @@ const Sharing: React.VoidFunctionComponent<SharingProps> = ({
<div className={clsx("card p-4", className)}>
<div className="mb-1 flex items-center justify-between">
<div className="text-lg font-semibold text-slate-700">
Share via link
{t("shareLink")}
</div>
<button
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"
>
Hide
{t("hide")}
</button>
</div>
<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"
>
{didCopy ? "Copied" : "Copy Link"}
{didCopy ? t("copied") : t("copyLink")}
</Button>
</div>
</div>

View file

@ -1,6 +1,7 @@
import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import React from "react";
import Menu from "@/components/icons/menu.svg";
@ -40,6 +41,7 @@ const MobileNavigation: React.VoidFunctionComponent<{
openLoginModal: () => void;
}> = ({ openLoginModal }) => {
const { user } = useSession();
const { t } = useTranslation("app");
return (
<div
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"
>
<Login className="h-5 opacity-75" />
<span className="inline-block">Login</span>
<span className="inline-block">{t("login")}</span>
</button>
)}
<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"
>
<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>
}
>
@ -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"
>
<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>
}
>
@ -121,12 +123,13 @@ const MobileNavigation: React.VoidFunctionComponent<{
const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
className,
}) => {
const { t } = useTranslation("app");
return (
<div className={clsx("space-y-1", className)}>
<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">
<Pencil className="h-5 opacity-75 " />
<span className="inline-block">New Poll</span>
<span className="inline-block">{t("newPoll")}</span>
</a>
</Link>
<a
@ -136,7 +139,7 @@ const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
rel="noreferrer"
>
<Support className="h-5 opacity-75" />
<span className="inline-block">Support</span>
<span className="inline-block">{t("support")}</span>
</a>
</div>
);
@ -146,6 +149,7 @@ const UserDropdown: React.VoidFunctionComponent<
DropdownProps & { openLoginModal: () => void }
> = ({ children, openLoginModal, ...forwardProps }) => {
const { logout, user } = useSession();
const { t } = useTranslation("app");
const modalContext = useModalContext();
if (!user) {
return null;
@ -175,17 +179,14 @@ const UserDropdown: React.VoidFunctionComponent<
</div>
</div>
</div>
<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>
<p>{t("guestSessionNotice")}</p>
<div>
<a
href="https://support.rallly.co/guest-sessions"
target="_blank"
rel="noreferrer"
>
Read more about guest sessions.
{t("guestSessionReadMore")}
</a>
</div>
</div>
@ -204,20 +205,19 @@ const UserDropdown: React.VoidFunctionComponent<
) : null}
<DropdownItem
icon={Logout}
label={user.isGuest ? "Forget me" : "Logout"}
label={user.isGuest ? t("forgetMe") : t("logout")}
onClick={() => {
if (user?.isGuest) {
modalContext.render({
title: "Are you sure?",
description:
"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.",
title: t("areYouSure"),
description: t("endingGuestSessionNotice"),
onOk: logout,
okButtonProps: {
type: "danger",
},
okText: "End session",
cancelText: "Cancel",
okText: t("endSession"),
cancelText: t("cancel"),
});
} else {
logout();
@ -232,6 +232,7 @@ const StandardLayout: React.VoidFunctionComponent<{
children?: React.ReactNode;
}> = ({ children, ...rest }) => {
const { user } = useSession();
const { t } = useTranslation("app");
const [loginModal, openLoginModal] = useModal({
footer: null,
overlayClosable: true,
@ -255,7 +256,7 @@ const StandardLayout: React.VoidFunctionComponent<{
<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">
<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>
</Link>
<a
@ -265,14 +266,14 @@ const StandardLayout: React.VoidFunctionComponent<{
rel="noreferrer"
>
<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>
<Popover
placement="right-start"
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">
<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" />
</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"
>
<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>
)}
</div>
@ -311,7 +312,7 @@ const StandardLayout: React.VoidFunctionComponent<{
{user.shortName}
</div>
<div className="truncate text-xs text-slate-500">
{user.isGuest ? "Guest" : "User"}
{user.isGuest ? t("guest") : t("user")}
</div>
</div>
<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"
rel="noreferrer"
>
Support
{t("support")}
</a>
<Link href="https://github.com/lukevella/rallly/discussions">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
Discussions
{t("discussions")}
</a>
</Link>
<Link href="https://blog.rallly.co">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
Blog
{t("blog")}
</a>
</Link>
<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">
<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" />
<span>Donate</span>
<span>{t("donate")}</span>
</a>
</Link>
</div>

View file

@ -7,7 +7,7 @@ export const getStaticProps: GetStaticProps = async ({ locale = "en" }) => {
try {
return {
props: {
...(await serverSideTranslations(locale, ["app", "homepage"])),
...(await serverSideTranslations(locale, ["homepage"])),
},
};
} 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"),
).toBeVisible();
await page.type(
"[placeholder='Thanks for the invite!']",
"[placeholder='Leave a comment on this poll (visible to everyone)']",
"This is a comment!",
);
await page.type('[placeholder="Your name…"]', "Test user");