From ab150e3ee4231f1162c0171a5fe3f0a7dbed2506 Mon Sep 17 00:00:00 2001 From: Giacomo Materozzi Date: Tue, 27 Feb 2024 23:49:44 +0100 Subject: [PATCH] feat(translation): crud language --- app/api/translations/[translationId]/route.ts | 5 +- components/slide-over.tsx | 6 +- .../dialogs/add-edit-languages.tsx | 118 ++++++++++++++++++ .../translation/dialogs/add-new-languages.tsx | 116 +++++++++++++++++ components/translation/index.tsx | 12 +- .../translation/settings-slide-over.tsx | 33 ++++- components/translation/table/row.tsx | 4 +- components/translation/useTranslation.tsx | 27 +++- docker-compose.yml | 2 + store/useI18nState.ts | 53 +++++++- styles/globals.css | 4 + 11 files changed, 361 insertions(+), 19 deletions(-) create mode 100644 components/translation/dialogs/add-edit-languages.tsx create mode 100644 components/translation/dialogs/add-new-languages.tsx diff --git a/app/api/translations/[translationId]/route.ts b/app/api/translations/[translationId]/route.ts index 6a3523b4..75aca4fd 100644 --- a/app/api/translations/[translationId]/route.ts +++ b/app/api/translations/[translationId]/route.ts @@ -11,7 +11,10 @@ const routeContextSchema = z.object({ }), }) -export async function DELETE(context: z.infer) { +export async function DELETE( + _req: Request, + context: z.infer +) { try { // Validate the route params. const { params } = routeContextSchema.parse(context) diff --git a/components/slide-over.tsx b/components/slide-over.tsx index 07e02639..ca089dab 100644 --- a/components/slide-over.tsx +++ b/components/slide-over.tsx @@ -115,11 +115,7 @@ export type SlideOverButtonProps = { } export const SlideOverButton = (props: SlideOverButtonProps) => ( - + + + + + + + + ) +} + +export default EditLanguage diff --git a/components/translation/dialogs/add-new-languages.tsx b/components/translation/dialogs/add-new-languages.tsx new file mode 100644 index 00000000..308da3e0 --- /dev/null +++ b/components/translation/dialogs/add-new-languages.tsx @@ -0,0 +1,116 @@ +import { ChangeEvent, useCallback, useState } from "react" +import { Language } from "@/store/useI18nState" +import { DialogClose } from "@radix-ui/react-dialog" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" + +type Props = { + addLanguage: (language: Language) => void +} + +const AddNewLanguage = (props: Props) => { + const { addLanguage } = props + + const [languageName, setLanguageName] = useState("") + const [shortName, setShortName] = useState("") + + const handleChangeLanguageName = useCallback( + (e: ChangeEvent) => { + setLanguageName(e.target.value) + }, + [] + ) + + const handleChangeShortName = useCallback( + (e: ChangeEvent) => { + setShortName(e.target.value) + }, + [] + ) + + const reset = useCallback(() => { + setLanguageName("") + setShortName("") + }, []) + + const onSubmit = useCallback(() => { + addLanguage({ + lang: languageName, + short: shortName, + }) + reset() + }, [addLanguage, languageName, reset, shortName]) + + return ( + + + + + + + New language + +
+
+ + +
+
+ + +
+
+ + + + + +
+
+ ) +} + +export default AddNewLanguage diff --git a/components/translation/index.tsx b/components/translation/index.tsx index e2014a5e..c75b597f 100644 --- a/components/translation/index.tsx +++ b/components/translation/index.tsx @@ -27,12 +27,16 @@ export function Editor(props: EditorProps) { keywords, isSaving, title, + languages, save, addNewKey, deleteKey, editTranslation, setTitle, editContext, + addLanguage, + editLanguage, + deleteLanguage, } = useTranslation(props) const [isProjectSettingsOpened, openProjectSettings] = @@ -85,7 +89,13 @@ export function Editor(props: EditorProps) { /> {isProjectSettingsOpened && ( - openProjectSettings(false)} /> + openProjectSettings(false)} + /> )} ) diff --git a/components/translation/settings-slide-over.tsx b/components/translation/settings-slide-over.tsx index bcfbab2f..b0091561 100644 --- a/components/translation/settings-slide-over.tsx +++ b/components/translation/settings-slide-over.tsx @@ -1,17 +1,44 @@ +import { EditLanguageType, Language } from "@/store/useI18nState" + import SlideOver, { SlideOverButton, SlideOverRow } from "../slide-over" +import EditLanguage from "./dialogs/add-edit-languages" +import AddNewLanguage from "./dialogs/add-new-languages" type Props = { + languages: Language[] + addLanguage: (language: Language) => void + editLanguage: (language: EditLanguageType) => void + deleteLanguage: (language: Language) => void onClose: () => void } const ProjectSettingsSlideOver = (props: Props) => { - const { onClose } = props + const { languages, addLanguage, editLanguage, deleteLanguage, onClose } = + props + { + languages.map((language) => ( + + )) + } return ( - {}}> +
- {}} /> + <> + {languages.map((language) => ( + + ))} + + +
diff --git a/components/translation/table/row.tsx b/components/translation/table/row.tsx index 70137d99..1d04c446 100644 --- a/components/translation/table/row.tsx +++ b/components/translation/table/row.tsx @@ -31,7 +31,7 @@ const Row = (props: Props) => { key={language.language} className="text-green-600 dark:text-green-500 font-medium" > - {language.language.toUpperCase()} + {language.short.toUpperCase()} ) } @@ -41,7 +41,7 @@ const Row = (props: Props) => { key={language.language} className="text-red-600 dark:text-red-500 font-medium" > - {language.language.toUpperCase()} + {language.short.toUpperCase()} ) })} diff --git a/components/translation/useTranslation.tsx b/components/translation/useTranslation.tsx index 4683dd76..8f5e290c 100644 --- a/components/translation/useTranslation.tsx +++ b/components/translation/useTranslation.tsx @@ -1,6 +1,11 @@ import { useCallback, useEffect, useMemo, useState } from "react" import { useRouter } from "next/navigation" -import { I18nInfo, I18nLang, useI18nState } from "@/store/useI18nState" +import { + I18nInfo, + I18nLang, + Language, + useI18nState, +} from "@/store/useI18nState" import { EditorProps } from "." import { toast } from "../ui/use-toast" @@ -9,11 +14,13 @@ import { NewKeyword } from "./dialogs/add-new-keyword" type LanguagesAvailable = { language: string available: boolean + short: string } type KeywordLanguage = { value: string language: string + short: string } export type Keyword = { @@ -32,6 +39,9 @@ const useTranslation = (props: EditorProps) => { setI18n, setTitle, editContext, + addLanguage, + editLanguage, + deleteLanguage, } = useI18nState() const keywords = useMemo((): Keyword[] => { @@ -48,10 +58,12 @@ const useTranslation = (props: EditorProps) => { return { value: language.keywords[keyword], language: language.lang, + short: language.short, } }), languagesAvailable: (i18n.languages as I18nLang[]).map((language) => ({ language: language.lang, + short: language.short, available: !!language.keywords[keyword], })), })) @@ -157,9 +169,19 @@ const useTranslation = (props: EditorProps) => { }) }, [i18n, props.translation.id, router]) + const languages: Language[] = useMemo( + () => + i18n.languages.map((language) => ({ + lang: language.lang, + short: language.short, + })), + [i18n.languages] + ) + return { title: i18n.title, keywords, + languages, editTranslation, addNewKey, deleteKey, @@ -167,6 +189,9 @@ const useTranslation = (props: EditorProps) => { isSaving, setTitle, editContext, + addLanguage, + editLanguage, + deleteLanguage, } } diff --git a/docker-compose.yml b/docker-compose.yml index 622b4fa2..cf453d6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: image: postgres:16.1 ports: - 5555:5432 + volumes: + - db:/var/lib/psql environment: - POSTGRES_DB=translo - POSTGRES_USER=usr diff --git a/store/useI18nState.ts b/store/useI18nState.ts index 24f9be14..d0c9791e 100644 --- a/store/useI18nState.ts +++ b/store/useI18nState.ts @@ -3,22 +3,33 @@ import { create } from "zustand" import { NewKeyword } from "@/components/translation/dialogs/add-new-keyword" +export type Language = { + lang: string + short: string +} + +export type EditLanguageType = Language & { exShortName: string } + export type I18nInfo = { key: string context: string } -export type I18nLang = { - lang: string + +export type I18nLang = Language & { keywords: Record } + export type I18n = Pick & { languages: I18nLang[] info: I18nInfo[] } + export type I18nState = { i18n: I18n setTitle: (title: string) => void - addLanguage: (language: string) => void + addLanguage: (language: Language) => void + editLanguage: (Language: EditLanguageType) => void + deleteLanguage: (language: Language) => void editContext: (key: string, context: string) => void editTranslation: (language: string, key: string, value: string) => void addKey: (keyword: NewKeyword) => void @@ -32,7 +43,8 @@ export const initialI18nState: I18n = { info: [], languages: [ { - lang: "en", + lang: "English", + short: "en", keywords: {}, }, ], @@ -48,11 +60,40 @@ export const useI18nState = create()((set) => ({ }, })) }, - addLanguage: (language: string) => + addLanguage: (language: Language) => + set((state) => ({ + i18n: { + ...state.i18n, + languages: [ + ...state.i18n.languages, + { lang: language.lang, short: language.short, keywords: {} }, + ], + }, + })), + editLanguage: (languageToEdit: EditLanguageType) => + set((state) => ({ + i18n: { + ...state.i18n, + languages: state.i18n.languages.map((language) => { + if (language.short !== languageToEdit.exShortName) { + return language + } + + return { + ...language, + lang: languageToEdit.lang, + short: languageToEdit.short, + } + }), + }, + })), + deleteLanguage: (languageToDelete: Language) => set((state) => ({ i18n: { ...state.i18n, - languages: [...state.i18n.languages, { lang: language, keywords: {} }], + languages: (state.i18n.languages as I18nLang[]).filter( + (language) => language.short !== languageToDelete.short + ), }, })), deleteKey: (key: string) => { diff --git a/styles/globals.css b/styles/globals.css index b55f7747..edfbd6f6 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -83,3 +83,7 @@ .t-textarea { @apply py-2 px-3 block w-full border-gray-200 border-2 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400 dark:focus:ring-gray-600; } + +.t-button { + @apply max-w-max py-2 px-3 inline-flex items-center gap-x-2 text-[13px] font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-gray-700 dark:text-white dark:hover:bg-gray-800 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600; +}