2021-07-12 17:06:27 +02:00
|
|
|
import { useEffect, useReducer, useCallback, FC, ChangeEvent } from "react";
|
2021-03-10 01:16:52 +01:00
|
|
|
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
|
2021-03-11 13:29:22 +01:00
|
|
|
import Router from "next/router";
|
2021-07-12 17:06:27 +02:00
|
|
|
import dynamic from "next/dynamic";
|
2021-03-18 14:18:29 +01:00
|
|
|
import { Stack, VStack, HStack, IconButton } from "@chakra-ui/react";
|
2021-03-16 01:33:19 +01:00
|
|
|
import { FaExchangeAlt } from "react-icons/fa";
|
2021-07-12 17:06:27 +02:00
|
|
|
import { HiTranslate } from "react-icons/hi";
|
2021-04-30 23:11:26 +02:00
|
|
|
import { useHotkeys } from "react-hotkeys-hook";
|
2021-06-12 22:44:56 +02:00
|
|
|
import { CustomHead, LangSelect, TranslationArea } from "@components";
|
|
|
|
|
import { useToastOnLoad } from "@hooks";
|
|
|
|
|
import { googleScrape, extractSlug, textToSpeechScrape } from "@utils/translate";
|
2021-08-30 21:35:22 +02:00
|
|
|
import { retrieveFromType, replaceBoth } from "@utils/language";
|
2021-06-12 22:44:56 +02:00
|
|
|
import langReducer, { Actions, initialState } from "@utils/reducer";
|
2021-10-10 21:45:14 +02:00
|
|
|
import { localGetItem, localSetItem } from "@utils/storage";
|
2021-03-10 01:16:52 +01:00
|
|
|
|
2021-07-12 17:06:27 +02:00
|
|
|
const AutoTranslateButton = dynamic(() => import("@components/AutoTranslateButton"), { ssr: false });
|
|
|
|
|
|
2021-03-28 23:17:47 +02:00
|
|
|
const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, translationRes, audio, errorMsg, initial }) => {
|
2021-04-01 15:40:25 +02:00
|
|
|
const [{ source, target, query, delayedQuery, translation, isLoading }, dispatch] = useReducer(langReducer, initialState);
|
2021-03-10 19:27:36 +01:00
|
|
|
|
2021-03-16 01:33:19 +01:00
|
|
|
const handleChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLSelectElement>) => {
|
2021-03-10 19:27:36 +01:00
|
|
|
dispatch({
|
|
|
|
|
type: Actions.SET_FIELD,
|
|
|
|
|
payload: {
|
|
|
|
|
key: e.target.id,
|
|
|
|
|
value: e.target.value
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
2021-03-10 01:16:52 +01:00
|
|
|
|
2021-07-12 17:06:27 +02:00
|
|
|
const changeRoute = useCallback((customQuery: string) => {
|
|
|
|
|
if (isLoading)
|
|
|
|
|
return;
|
|
|
|
|
if (!customQuery || customQuery === initialState.query)
|
|
|
|
|
return;
|
|
|
|
|
if (!home && !initial)
|
|
|
|
|
return;
|
|
|
|
|
if (!home && customQuery === initial.query && source === initial.source && target === initial.target)
|
|
|
|
|
return;
|
|
|
|
|
|
2021-10-10 21:45:14 +02:00
|
|
|
localSetItem("source", source);
|
|
|
|
|
localSetItem("target", target);
|
|
|
|
|
|
2021-07-12 17:06:27 +02:00
|
|
|
dispatch({ type: Actions.SET_FIELD, payload: { key: "isLoading", value: true }});
|
|
|
|
|
Router.push(`/${source}/${target}/${encodeURIComponent(customQuery)}`);
|
|
|
|
|
}, [isLoading, source, target, home, initial]);
|
|
|
|
|
|
2021-03-10 01:16:52 +01:00
|
|
|
useEffect(() => {
|
2021-04-01 15:40:25 +02:00
|
|
|
if (home)
|
2021-10-10 21:45:14 +02:00
|
|
|
return dispatch({
|
|
|
|
|
type: Actions.SET_ALL,
|
|
|
|
|
payload: {
|
|
|
|
|
state: {
|
|
|
|
|
...initialState,
|
|
|
|
|
source: localGetItem("source") || initialState.source,
|
|
|
|
|
target: localGetItem("target") || initialState.target,
|
|
|
|
|
isLoading: false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2021-04-01 15:40:25 +02:00
|
|
|
if (!initial)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
|
type: Actions.SET_ALL,
|
2021-10-10 21:45:14 +02:00
|
|
|
payload: {
|
|
|
|
|
state: {
|
|
|
|
|
...initial,
|
|
|
|
|
delayedQuery: initial.query,
|
|
|
|
|
translation: translationRes,
|
|
|
|
|
isLoading: false
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-04-01 15:40:25 +02:00
|
|
|
});
|
|
|
|
|
}, [initial, translationRes, home]);
|
2021-03-10 01:16:52 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2021-03-18 23:47:12 +01:00
|
|
|
const timeout = setTimeout(() =>
|
|
|
|
|
dispatch({ type: Actions.SET_FIELD, payload: { key: "delayedQuery", value: query }}
|
|
|
|
|
), 1000);
|
2021-03-10 01:16:52 +01:00
|
|
|
return () => clearTimeout(timeout);
|
2021-03-13 12:00:41 +01:00
|
|
|
}, [query]);
|
2021-03-09 20:33:12 +01:00
|
|
|
|
2021-04-01 15:40:25 +02:00
|
|
|
useEffect(() => {
|
2021-10-10 21:45:14 +02:00
|
|
|
const handler = (url: string) => {
|
|
|
|
|
url === Router.asPath || dispatch({ type: Actions.SET_FIELD, payload: { key: "isLoading", value: true }});
|
|
|
|
|
|
|
|
|
|
if (url !== "/")
|
|
|
|
|
return;
|
|
|
|
|
dispatch({ type: Actions.SET_FIELD, payload: { key: "source", value: initialState.source }});
|
|
|
|
|
localSetItem("source", initialState.source);
|
|
|
|
|
dispatch({ type: Actions.SET_FIELD, payload: { key: "target", value: initialState.target }});
|
|
|
|
|
localSetItem("target", initialState.target);
|
|
|
|
|
};
|
2021-04-01 15:40:25 +02:00
|
|
|
Router.events.on("beforeHistoryChange", handler);
|
|
|
|
|
return () => Router.events.off("beforeHistoryChange", handler);
|
|
|
|
|
}, []);
|
2021-03-10 18:15:57 +01:00
|
|
|
|
2021-08-30 21:35:22 +02:00
|
|
|
const sourceLangs = retrieveFromType("source");
|
|
|
|
|
const targetLangs = retrieveFromType("target");
|
2021-03-19 00:20:28 +01:00
|
|
|
const { source: transLang, target: queryLang } = replaceBoth("exception", { source: target, target: source });
|
2021-03-10 18:15:57 +01:00
|
|
|
|
2021-03-16 13:02:23 +01:00
|
|
|
useToastOnLoad({
|
|
|
|
|
title: "Unexpected error",
|
|
|
|
|
description: errorMsg,
|
|
|
|
|
status: "error",
|
|
|
|
|
updateDeps: initial
|
|
|
|
|
});
|
|
|
|
|
|
2021-04-30 23:11:26 +02:00
|
|
|
const canSwitch = source !== "auto" && !isLoading;
|
|
|
|
|
|
|
|
|
|
useHotkeys("ctrl+shift+s, command+shift+s, ctrl+shift+f, command+shift+f", () => (
|
|
|
|
|
canSwitch && dispatch({ type: Actions.SWITCH_LANGS })
|
|
|
|
|
), [canSwitch]);
|
|
|
|
|
|
2021-03-28 23:17:47 +02:00
|
|
|
return (
|
2021-06-12 22:44:56 +02:00
|
|
|
<>
|
|
|
|
|
<CustomHead home={home} />
|
|
|
|
|
|
2021-03-18 14:18:29 +01:00
|
|
|
<VStack px={[8, null, 24, 40]} w="full">
|
|
|
|
|
<HStack px={[1, null, 3, 4]} w="full">
|
|
|
|
|
<LangSelect
|
|
|
|
|
id="source"
|
|
|
|
|
aria-label="Source language"
|
|
|
|
|
value={source}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
langs={sourceLangs}
|
|
|
|
|
/>
|
|
|
|
|
<IconButton
|
|
|
|
|
aria-label="Switch languages"
|
|
|
|
|
icon={<FaExchangeAlt />}
|
|
|
|
|
colorScheme="lingva"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => dispatch({ type: Actions.SWITCH_LANGS })}
|
2021-04-30 23:11:26 +02:00
|
|
|
isDisabled={!canSwitch}
|
2021-03-18 14:18:29 +01:00
|
|
|
/>
|
|
|
|
|
<LangSelect
|
|
|
|
|
id="target"
|
|
|
|
|
aria-label="Target language"
|
|
|
|
|
value={target}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
langs={targetLangs}
|
|
|
|
|
/>
|
|
|
|
|
</HStack>
|
|
|
|
|
<Stack direction={["column", null, "row"]} w="full">
|
|
|
|
|
<TranslationArea
|
|
|
|
|
id="query"
|
|
|
|
|
aria-label="Translation query"
|
|
|
|
|
placeholder="Text"
|
|
|
|
|
value={query}
|
2021-04-01 15:40:25 +02:00
|
|
|
onChange={e => isLoading || handleChange(e)}
|
2021-07-12 17:06:27 +02:00
|
|
|
onSubmit={useCallback(() => changeRoute(query), [query, changeRoute])}
|
2021-03-19 00:20:28 +01:00
|
|
|
lang={queryLang}
|
2021-03-25 16:48:46 +01:00
|
|
|
audio={audio?.source}
|
2021-03-18 14:18:29 +01:00
|
|
|
/>
|
2021-07-12 17:06:27 +02:00
|
|
|
<Stack direction={["row", null, "column"]} justify="center" spacing={3} px={[2, null, "initial"]}>
|
|
|
|
|
<IconButton
|
|
|
|
|
aria-label="Translate"
|
|
|
|
|
icon={<HiTranslate />}
|
|
|
|
|
colorScheme="lingva"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => changeRoute(query)}
|
|
|
|
|
isDisabled={isLoading}
|
|
|
|
|
w={["full", null, "auto"]}
|
|
|
|
|
/>
|
|
|
|
|
<AutoTranslateButton
|
|
|
|
|
onAuto={useCallback(() => changeRoute(delayedQuery), [delayedQuery, changeRoute])}
|
|
|
|
|
isDisabled={isLoading}
|
|
|
|
|
w={["full", null, "auto"]}
|
|
|
|
|
/>
|
|
|
|
|
</Stack>
|
2021-03-18 14:18:29 +01:00
|
|
|
<TranslationArea
|
|
|
|
|
id="translation"
|
|
|
|
|
aria-label="Translation result"
|
|
|
|
|
placeholder="Translation"
|
|
|
|
|
value={translation ?? ""}
|
|
|
|
|
readOnly={true}
|
2021-03-19 00:20:28 +01:00
|
|
|
lang={transLang}
|
2021-03-25 16:48:46 +01:00
|
|
|
audio={audio?.target}
|
|
|
|
|
canCopy={true}
|
2021-04-01 15:40:25 +02:00
|
|
|
isLoading={isLoading}
|
2021-03-18 14:18:29 +01:00
|
|
|
/>
|
|
|
|
|
</Stack>
|
2021-03-16 01:33:19 +01:00
|
|
|
</VStack>
|
2021-06-12 22:44:56 +02:00
|
|
|
</>
|
2021-03-09 20:33:12 +01:00
|
|
|
);
|
2021-06-12 22:44:56 +02:00
|
|
|
};
|
2021-03-09 20:33:12 +01:00
|
|
|
|
|
|
|
|
export default Page;
|
2021-03-10 01:16:52 +01:00
|
|
|
|
|
|
|
|
export const getStaticPaths: GetStaticPaths = async () => ({
|
|
|
|
|
paths: [
|
|
|
|
|
{
|
|
|
|
|
params: { slug: [] }
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
fallback: true
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-11 13:29:22 +01:00
|
|
|
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
2021-03-10 01:16:52 +01:00
|
|
|
if (!params?.slug || !Array.isArray(params.slug))
|
|
|
|
|
return {
|
2021-03-18 23:47:12 +01:00
|
|
|
props: { home: true }
|
2021-03-10 01:16:52 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const { source, target, query } = extractSlug(params.slug);
|
|
|
|
|
|
|
|
|
|
if (!query)
|
|
|
|
|
return {
|
|
|
|
|
notFound: true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!source || !target)
|
|
|
|
|
return {
|
|
|
|
|
redirect: {
|
|
|
|
|
destination: `/${source ?? "auto"}/${target ?? "en"}/${query}`,
|
|
|
|
|
permanent: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 16:48:46 +01:00
|
|
|
const textScrape = await googleScrape(source, target, query);
|
|
|
|
|
|
|
|
|
|
const [sourceAudio, targetAudio] = await Promise.all([
|
|
|
|
|
textToSpeechScrape(source, query),
|
2021-08-30 21:35:22 +02:00
|
|
|
"translationRes" in textScrape
|
|
|
|
|
? textToSpeechScrape(target, textScrape.translationRes)
|
|
|
|
|
: null
|
2021-03-25 16:48:46 +01:00
|
|
|
]);
|
2021-03-16 13:02:23 +01:00
|
|
|
|
2021-03-10 01:16:52 +01:00
|
|
|
return {
|
|
|
|
|
props: {
|
2021-03-25 16:48:46 +01:00
|
|
|
...textScrape,
|
|
|
|
|
audio: {
|
|
|
|
|
source: sourceAudio,
|
|
|
|
|
target: targetAudio
|
|
|
|
|
},
|
2021-03-10 01:16:52 +01:00
|
|
|
initial: {
|
|
|
|
|
source, target, query
|
|
|
|
|
}
|
|
|
|
|
},
|
2021-08-30 21:35:22 +02:00
|
|
|
revalidate: !("errorMsg" in textScrape)
|
2021-03-16 13:02:23 +01:00
|
|
|
? 2 * 30 * 24 * 60 * 60 // 2 months
|
|
|
|
|
: 1
|
2021-03-10 01:16:52 +01:00
|
|
|
};
|
2021-06-12 22:44:56 +02:00
|
|
|
};
|