···6060bun dev
6161```
62626363-If you work on this project, please note that nextui is being phased out in favor of shadcn/ui, so please use the latter for new components.
6464-6563## Deploy
6664To build and run the website use
6765```bash
+81-57
app/(home)/faq.component.tsx
···11"use client";
2233-import LinkTag from "@/components/link-tag";
43import { Section } from "@/components/section";
55-import { Accordion, AccordionItem, Code } from "@nextui-org/react";
66-import { useCookies } from "next-client-cookies";
44+import {
55+ Accordion,
66+ AccordionContent,
77+ AccordionItem,
88+ AccordionTrigger
99+} from "@/components/ui/accordion";
1010+import { Anchor, Code, Ol } from "@/components/ui/typography";
1111+import { isValidElement } from "react";
712import { HiBell, HiCash, HiChat, HiLockClosed, HiUserAdd } from "react-icons/hi";
813914const data = [
···1217 title: "How do I invite Wamellow to my Server?",
1318 subtitle: "Invite Wamellow to your server to get started!",
1419 content: (
1515- <ol
1616- className="list-decimal list-inside marker:text-neutral-500"
1717- itemProp="text"
1818- >
2020+ <Ol itemProp="text">
1921 <li>
2020- Be sure to have the <Code color="secondary">Manage Server</Code> permission on the server you want <LinkTag href="/login?invite=true">invite Wamellow</LinkTag> into.
2222+ Be sure to have the <Code>Manage Server</Code> permission on the server you want <Anchor href="/login?invite=true" target="_blank">invite Wamellow</Anchor> into.
2123 </li>
2224 <li>
2323- Open Discord{"'"}s add-app flow at <LinkTag href="/login?invite=true">wamellow.com/invite</LinkTag>.
2525+ Open Discord{"'"}s add-app flow at <Anchor href="/login?invite=true" target="_blank">wamellow.com/invite</Anchor>.
2426 </li>
2527 <li>
2628 Select a server and click on {"\""}Continue{"\""}.
···3133 <li>
3234 <span className="font-semibold">Done!</span> 🎉 You should now find yourself on the Dashboard for your server!
3335 </li>
3434- </ol>
3636+ </Ol>
3537 )
3638 },
3739 {
3840 startContent: <HiCash />,
3941 title: "Is Text to Speech free to use?",
4042 content: (
4141- <div>
4242- Yes, Text to Speech is free to use. However, you might have to <LinkTag href="/vote">vote for Wamellow on top.gg</LinkTag> if you start using it a lot.
4343+ <div itemProp="text">
4444+ Yes, Text to Speech is free to use. However, you might have to <Anchor href="/vote" target="_blank">vote for Wamellow on top.gg</Anchor> if you start using it a lot.
4345 </div>
4446 )
4547 },
···5052 <div itemProp="text">
5153 <ol className="list-decimal list-inside marker:text-neutral-500 mb-4">
5254 <li>
5353- <LinkTag href="/login?invite=true">Invite Wamellow</LinkTag> to your Server. If you do not own it, ask the server Administrators to add Wamellow.
5555+ <Anchor href="/login?invite=true" target="_blank">Invite Wamellow</Anchor> to your Server. If you do not own it, ask the server Administrators to add Wamellow.
5456 </li>
5557 <li>
5656- Go to the <LinkTag href="/login?invite=true">Dashboard on wamellow.com/dashboard</LinkTag>, find your server and click {"\""}manage{"\""}.
5858+ Go to the <Anchor href="/login?invite=true" target="_blank">Dashboard on wamellow.com/dashboard</Anchor>, find your server and click {"\""}manage{"\""}.
5759 </li>
5860 <li>
5961 Select a channel to be used in the {"\""}Text to Speech{"\""} section.
···6971 </li>
7072 </ol>
71737272- You can also watch the video tutorial below or <LinkTag href="https://youtu.be/NS5fZ1ltovE?si=8hE1o6BBELxAxJbH">watch it on YouTube</LinkTag>.
7474+ You can also watch the video tutorial below or <Anchor href="https://youtu.be/NS5fZ1ltovE?si=8hE1o6BBELxAxJbH" target="_blank">watch it on YouTube</Anchor>.
7375 <iframe
7476 className="mt-2 aspect-video rounded-lg"
7577 width="100%"
···103105 </li>
104106 </ol>
105107106106- You can also watch the video tutorial below or <LinkTag href="https://youtu.be/ehc0_whydu8?si=8hE1o6BBELxAxJbH">watch it on YouTube</LinkTag>.
108108+ You can also watch the video tutorial below or <Anchor href="https://youtu.be/ehc0_whydu8?si=8hE1o6BBELxAxJbH" target="_blank">watch it on YouTube</Anchor>.
107109 <iframe
108110 className="mt-2 aspect-video rounded-lg"
109111 width="100%"
···125127 }
126128];
127129128128-interface Props {
129129- showTitle?: boolean;
130130-}
130130+const schema = {
131131+ "@context": "https://schema.org",
132132+ "@type": "FAQPage",
133133+ mainEntity: data.map((item) => ({
134134+ "@type": "Question",
135135+ name: item.title,
136136+ acceptedAnswer: {
137137+ "@type": "Answer",
138138+ text: extractText(item.content)
139139+ }
140140+ }))
141141+};
131142132143export function Faq({
133144 showTitle = false
134134-}: Props) {
135135- const cookies = useCookies();
136136-145145+}: {
146146+ showTitle?: boolean;
147147+}) {
137148 return (
138138- <div
139139- className="my-4 w-full"
140140- itemType="https://schema.org/FAQPage"
141141- itemScope
142142- >
149149+ <div>
150150+ <script
151151+ type="application/ld+json"
152152+ dangerouslySetInnerHTML={{
153153+ __html: JSON.stringify(schema)
154154+ }}
155155+ />
143156144144- {showTitle
145145- ?
157157+ {showTitle && (
146158 <Section
147159 className="mb-4"
148160 title="Frequently Asked Questions about Wamellow"
149161 >
150162 Commonly asked questions about Wamellow and how to use it.
151163 </Section>
152152- :
153153- <b className="sr-only">
154154- Frequently Asked Questions for Wamellow
155155- </b>
156156- }
164164+ )}
157165158166 <Accordion
159159- className="rounded-lg overflow-hidden"
160160- variant="splitted"
161161- defaultExpandedKeys={["0"]}
162162- disableAnimation={cookies.get("reduceMotions") === "true"}
167167+ type="single"
168168+ collapsible
169169+ defaultValue="0"
163170 >
164171 {data.map((item, index) => (
165172 <AccordionItem
166166- aria-label={item.title}
167167- className="!bg-wamellow"
168168- classNames={{ content: "mb-2 space-y-4" }}
173173+ value={index.toString()}
169174 key={index}
170170- startContent={item.startContent}
171171- subtitle={item.subtitle}
172172- title={
173173- <span itemProp="name">
174174- {item.title}
175175- </span>
176176- }
177177- itemType="https://schema.org/Question"
178178- itemProp="mainEntity"
179179- itemScope
180175 >
181181- <span
182182- itemType="https://schema.org/Answer"
183183- itemProp="acceptedAnswer"
184184- itemScope
185185- >
176176+ <AccordionTrigger className="text-left">
177177+ <div className="flex items-start gap-3">
178178+ <div className="mt-1 text-lg">
179179+ {item.startContent}
180180+ </div>
181181+ <div>
182182+ <div itemProp="name">
183183+ {item.title}
184184+ </div>
185185+ {item.subtitle && (
186186+ <div className="text-sm text-muted-foreground font-normal">
187187+ {item.subtitle}
188188+ </div>
189189+ )}
190190+ </div>
191191+ </div>
192192+ </AccordionTrigger>
193193+ <AccordionContent className="mb-2 space-y-4">
186194 {item.content}
187187- </span>
195195+ </AccordionContent>
188196 </AccordionItem>
189197 ))}
190198 </Accordion>
191199 </div>
192200 );
201201+}
202202+203203+function extractText(content: React.ReactNode): string {
204204+ if (typeof content === "string") return content;
205205+ if (typeof content === "number") return content.toString();
206206+207207+ if (isValidElement(content)) {
208208+ if ((content.props as React.PropsWithChildren).children) {
209209+ return extractText((content.props as React.PropsWithChildren).children);
210210+ }
211211+ }
212212+ if (!Array.isArray(content)) return "";
213213+214214+ return content
215215+ .map((child) => extractText(child))
216216+ .join(" ");
193217}
+5-5
app/(home)/page.tsx
···1212import { Badge } from "@/components/ui/badge";
1313import { Button } from "@/components/ui/button";
1414import { Skeleton } from "@/components/ui/skeleton";
1515+import { Code } from "@/components/ui/typography";
1516import { defaultFetchOptions } from "@/lib/api";
1617import CaptchaPic from "@/public/captcha.webp";
1718import ArrowPic from "@/public/icons/arroww.webp";
···2526import { toFixedArrayLength } from "@/utils/fixed-array-length";
2627import { actor } from "@/utils/tts";
2728import { getCanonicalUrl } from "@/utils/urls";
2828-import { Code } from "@nextui-org/react";
2929import { Montserrat, Patrick_Hand } from "next/font/google";
3030import { headers } from "next/headers";
3131import Image from "next/image";
3232import Link from "next/link";
3333import { Suspense } from "react";
3434-import { BsYoutube } from "react-icons/bs";
3434+import { BsDiscord, BsYoutube } from "react-icons/bs";
3535import { HiArrowNarrowRight, HiArrowRight, HiCash, HiCheck, HiFire, HiLockOpen, HiUserAdd } from "react-icons/hi";
36363737import { Commands } from "./commands.component";
···130130 prefetch={false}
131131 href="/login?invite=true"
132132 >
133133- <HiUserAdd />
133133+ <HiUserAdd className="mx-1" />
134134 <span className="block sm:hidden">Invite</span>
135135 <span className="hidden sm:block">Invite Wamellow</span>
136136 </Link>
···143143 prefetch={false}
144144 href="/support"
145145 >
146146- <HiUserAdd />
146146+ <BsDiscord className="mx-1 mt-px" />
147147 <span className="block sm:hidden">Support</span>
148148 <span className="hidden sm:block">Join Support</span>
149149 </Link>
···220220 <h3 className={styles.h3}>97 Voices in 10 Languages</h3>
221221222222 <div className="pt-6">
223223- You can either generate files using <Code color="secondary">/tts file</Code>, talk in voice chats with <Code color="secondary">/tts voice</Code> or setup a dedicated channel!
223223+ You can either generate files using <Code>/tts file</Code>, talk in voice chats with <Code>/tts voice</Code> or setup a dedicated channel!
224224 Great for people with aphonia, dysphonia, or other speech impairments.
225225 </div>
226226
+26-26
app/(home)/status/side.component.tsx
···11"use client";
2233import DumbTextInput from "@/components/inputs/dumb-text-input";
44+import {
55+ Accordion,
66+ AccordionContent,
77+ AccordionItem,
88+ AccordionTrigger
99+} from "@/components/ui/accordion";
410import { Badge } from "@/components/ui/badge";
511import { intl } from "@/utils/numbers";
66-import { Accordion, AccordionItem } from "@nextui-org/react";
77-import { useCookies } from "next-client-cookies";
812import { type ReactNode, useEffect, useMemo, useState } from "react";
9131014import type { ApiV1StatusGetResponse } from "./api";
···1418}: {
1519 status: ApiV1StatusGetResponse;
1620}) {
1717- const cookies = useCookies();
1821 const [guildId, setGuildId] = useState<string>("");
19222023 const clusterId = useMemo(
···3538 return (
3639 <div className="flex flex-col gap-5">
3740 <Accordion
3838- variant="shadow"
3939- className="bg-wamellow"
4040- selectionMode="multiple"
4141- defaultExpandedKeys={["1"]}
4242- disableAnimation={cookies.get("reduceMotions") === "true"}
4141+ type="multiple"
4242+ defaultValue={["1"]}
4343+ variant="primary"
4344 >
4444- <AccordionItem
4545- key="1"
4646- aria-label="about"
4747- title="Performance & Usage"
4848- classNames={{ content: "mb-2 space-y-4" }}
4949- >
5050- <Row name="Uptime">
5151- {status.clusters[0].uptime}
5252- </Row>
5353- <Row name="Latency avg">
5454- {~~(status.clusters.reduce((prev, cur) => prev + cur.ping, 0) / status.clusters.length)}ms
5555- </Row>
5656- <Row name="Memory">
5757- {intl.format(~~(status.clusters.reduce((prev, cur) => prev + cur.memory, 0)))}mb
5858- </Row>
4545+ <AccordionItem value="1">
4646+ <AccordionTrigger>Performance & Usage</AccordionTrigger>
4747+ <AccordionContent className="mb-2 space-y-2">
4848+ <Row name="Uptime">
4949+ {status.clusters[0].uptime}
5050+ </Row>
5151+ <Row name="Latency avg">
5252+ {~~(status.clusters.reduce((prev, cur) => prev + cur.ping, 0) / status.clusters.length)}ms
5353+ </Row>
5454+ <Row name="Memory">
5555+ {intl.format(~~(status.clusters.reduce((prev, cur) => prev + cur.memory, 0)))}mb
5656+ </Row>
5757+ </AccordionContent>
5958 </AccordionItem>
6059 </Accordion>
61606261 <div>
6362 <DumbTextInput
6464- name="Find your Server's Cluster"
6565- placeholder="Copy & Paste your Server Id"
6363+ placeholder="Paste your Server Id"
6664 value={guildId}
6765 setValue={setGuildId}
6866 description={/^\d{15,20}$/.test(guildId) ? `Your guild is on cluster #${clusterId}.` : ""}
6967 />
70687171- Discord bots are divided into clusters or shards, which are logical processes running on the CPU, akin to multithreading.
6969+ <p className="text-muted-foreground text-sm -mt-2 px-0.5">
7070+ Discord bots are divided into clusters or shards, which are logical processes running on the CPU, akin to multithreading.
7171+ </p>
7272 </div>
7373 </div>
7474 );
···11-import { Accordion, AccordionItem } from "@nextui-org/react";
21import Link from "next/link";
32import { useParams } from "next/navigation";
44-import { useCookies } from "next-client-cookies";
53import { HiExternalLink, HiOutlineHand } from "react-icons/hi";
6455+import {
66+ Accordion,
77+ AccordionContent,
88+ AccordionItem,
99+ AccordionTrigger
1010+} from "./ui/accordion";
711import { Button } from "./ui/button";
1212+import { Anchor, Code } from "./ui/typography";
813914export function TTSFaq() {
1010- const cookies = useCookies();
1115 const params = useParams();
12161317 return (
1418 <Accordion
1919+ type="single"
2020+ collapsible
1521 className="lg:w-1/2"
1616- defaultExpandedKeys={["1"]}
1717- disableAnimation={cookies.get("reduceMotions") === "true"}
2222+ defaultValue="1"
1823 >
1919- <AccordionItem
2020- key="1"
2121- aria-label="how this works"
2222- title="How this works"
2323- >
2424- Users in a voice channel can send messages to this channel, and Wamellow will read them aloud in the voice channel. Please note that Wamellow can only be in one voice channel at a time.
2424+ <AccordionItem value="1">
2525+ <AccordionTrigger>Introduction</AccordionTrigger>
2626+ <AccordionContent>
2727+ Users in a voice channel can send messages to this channel, and Wamellow will read them aloud in the voice channel. Please note that Wamellow can only be in one voice channel at a time.
25282626- <iframe
2727- className="mt-4 aspect-video rounded-lg"
2828- width="100%"
2929- src="https://www.youtube.com/embed/NS5fZ1ltovE?si=uODiGspuNGKPRQKp"
3030- title="Wamellow Text to Speech tutorial"
3131- allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
3232- />
2929+ <iframe
3030+ className="mt-4 aspect-video rounded-lg"
3131+ width="100%"
3232+ src="https://www.youtube.com/embed/NS5fZ1ltovE?si=uODiGspuNGKPRQKp"
3333+ title="Wamellow Text to Speech tutorial"
3434+ allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
3535+ />
33363434- <Buttons guildId={params.guildId as string} />
3737+ <div className="flex gap-1.5 my-2">
3838+ <DocumentationLink />
3939+ <BlockWordsAndSlursButton guildId={params.guildId as string} />
4040+ </div>
4141+ </AccordionContent>
4242+ </AccordionItem>
4343+ <AccordionItem value="2">
4444+ <AccordionTrigger>Change Voice and Language</AccordionTrigger>
4545+ <AccordionContent className="space-y-3">
4646+ <p>
4747+ You can change your default voice <Anchor href="/profile/text-to-speech">in your personal settings</Anchor> or by running <Code>/tts set speaker</Code>.
4848+ </p>
4949+ <p>
5050+ If you only want to change the voice for a specific message, you can run <Code>/tts voice</Code> and provide the <Code>voice</Code> argument.
5151+ </p>
5252+ <p>
5353+ All available voices and languages are listed in the documentation. The language you choose will only affect messages you send.
5454+ </p>
5555+5656+ <DocumentationLink />
5757+ </AccordionContent>
3558 </AccordionItem>
3659 </Accordion>
3737- );
3838-}
3939-4040-function Buttons({ guildId }: { guildId: string; }) {
4141- return (
4242- <div className="flex gap-1.5 my-2">
4343- <DocumentationLink />
4444- <BlockWordsAndSlursButton guildId={guildId} />
4545- </div>
4660 );
4761}
4862
···130130 </tr>
131131 </thead>
132132 <tbody>
133133- <tr><td>🇺🇸 English (us)</td><td>Jessie</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_us_001.mp3" /></td></tr>
133133+ <tr><td>🇺🇸 English (us)</td><td>Jessie <strong>(TikTok, default until September 2025)</strong></td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_us_001.mp3" /></td></tr>
134134 <tr><td>🇺🇸 English (us)</td><td>Joey</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_us_006.mp3" /></td></tr>
135135 <tr><td>🇺🇸 English (us)</td><td>Professor</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_us_007.mp3" /></td></tr>
136136 <tr><td>🇺🇸 English (us)</td><td>Scientist</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_us_009.mp3" /></td></tr>
137137 <tr><td>🇺🇸 English (us)</td><td>Confidence</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_us_010.mp3" /></td></tr>
138138 <tr><td>🇺🇸 English (us)</td><td>Emotional</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_female_emotional.mp3" /></td></tr>
139139- <tr><td>🇺🇸 English (us)</td><td>Empathetic</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_female_samc.mp3" /></td></tr>
139139+ <tr><td>🇺🇸 English (us)</td><td>Empathetic <strong>(default)</strong></td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_female_samc.mp3" /></td></tr>
140140 <tr><td>🇺🇸 English (us)</td><td>Serious</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_male_cody.mp3" /></td></tr>
141141 <tr><td>🇺🇸 English (us)</td><td>Narration</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_male_narration.mp3" /></td></tr>
142142 <tr><td>🇺🇸 English (us)</td><td>Funny</td><td><audio controls src="https://r2.wamellow.com/tts-preview/en_male_funny.mp3" /></td></tr>
···244244<br />
245245<br />
246246247247-If Wamellow says that someone else's message is still being spoken, but it's not talking, use `/tts voice-stop`.
247247+If Wamellow says that someone else's message is still being spoken, but it's not talking, use `/tts stop`.
248248You can also use this to stop any message that is currently being spoken (i.e.: because of spam).