├── app
├── src
│ ├── vite-env.d.ts
│ ├── features
│ │ ├── ask
│ │ │ ├── index.ts
│ │ │ └── ui
│ │ │ │ └── ask.tsx
│ │ ├── word
│ │ │ ├── index.ts
│ │ │ └── ui
│ │ │ │ └── word_page.tsx
│ │ ├── edit_card
│ │ │ ├── index.ts
│ │ │ └── ui
│ │ │ │ └── edit_page.tsx
│ │ ├── login
│ │ │ ├── index.tsx
│ │ │ └── ui
│ │ │ │ └── login_page.tsx
│ │ ├── onboarding
│ │ │ ├── index.ts
│ │ │ └── ui
│ │ │ │ └── main.tsx
│ │ ├── profile
│ │ │ ├── index.ts
│ │ │ └── ui
│ │ │ │ └── profile.tsx
│ │ ├── create_card
│ │ │ ├── index.tsx
│ │ │ └── ui
│ │ │ │ └── main.tsx
│ │ ├── home
│ │ │ ├── index.ts
│ │ │ ├── store
│ │ │ │ └── card_store.ts
│ │ │ └── ui
│ │ │ │ ├── home.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── cards_list.tsx
│ │ │ │ └── search.tsx
│ │ ├── common
│ │ │ ├── index.tsx
│ │ │ ├── provider
│ │ │ │ └── auth.tsx
│ │ │ ├── store
│ │ │ │ └── auth.ts
│ │ │ └── ui
│ │ │ │ └── header.tsx
│ │ ├── play
│ │ │ ├── index.ts
│ │ │ ├── store
│ │ │ │ └── play_store.ts
│ │ │ └── ui
│ │ │ │ ├── goplay.tsx
│ │ │ │ └── play.tsx
│ │ └── international
│ │ │ ├── index.ts
│ │ │ ├── store
│ │ │ └── langs.ts
│ │ │ ├── ui
│ │ │ └── change_lang.tsx
│ │ │ ├── provider
│ │ │ └── i8_provider.tsx
│ │ │ ├── hooks
│ │ │ └── use_i8.ts
│ │ │ └── assets
│ │ │ ├── languages.ts
│ │ │ ├── cn_text.ts
│ │ │ ├── all_text.ts
│ │ │ ├── ru_text.ts
│ │ │ ├── tr_text.ts
│ │ │ ├── en_text.ts
│ │ │ └── fr_text.ts
│ ├── utils
│ │ ├── cn.ts
│ │ ├── string.ts
│ │ └── cookie.ts
│ ├── api
│ │ ├── get_token.ts
│ │ ├── play.ts
│ │ ├── me.ts
│ │ └── word.ts
│ ├── types
│ │ ├── words.ts
│ │ └── user.ts
│ ├── main.tsx
│ ├── routes.tsx
│ ├── index.css
│ └── components
│ │ └── ui
│ │ ├── dialog.tsx
│ │ ├── select.tsx
│ │ └── dropdown-menu.tsx
├── public
│ ├── chinese.png
│ ├── english.png
│ ├── french.png
│ ├── german.png
│ ├── russian.png
│ └── turkish.png
├── postcss.config.js
├── tsconfig.node.json
├── index.html
├── .gitignore
├── components.json
├── eslint.config.js
├── tsconfig.json
├── vite.config.ts
├── package.json
└── tailwind.config.js
├── landing
├── src
│ ├── env.d.ts
│ ├── components
│ │ ├── Whyus.astro
│ │ ├── Whyus_card.astro
│ │ ├── CTA.astro
│ │ ├── ChangeLang.tsx
│ │ ├── Paradox.astro
│ │ ├── Header.astro
│ │ ├── Hero.astro
│ │ └── Footer.astro
│ ├── layouts
│ │ └── Layout.astro
│ ├── icons
│ │ ├── Trust.astro
│ │ ├── Click.astro
│ │ ├── Info.astro
│ │ └── Repeat.astro
│ └── pages
│ │ ├── zh
│ │ └── index.astro
│ │ ├── index.astro
│ │ ├── ru
│ │ └── index.astro
│ │ ├── en
│ │ └── index.astro
│ │ ├── tr
│ │ └── index.astro
│ │ └── fr
│ │ └── index.astro
├── public
│ ├── hero.jpg
│ ├── en_paradox.png
│ ├── fr_paradox.png
│ ├── ru_paradox.png
│ ├── tr_paradox.png
│ ├── zh_paradox.png
│ └── favicon.svg
├── tsconfig.json
├── astro.config.mjs
├── .gitignore
├── tailwind.config.mjs
├── package.json
└── README.md
├── Makefile
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── devrun.sh
└── nginx.conf
/app/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/landing/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/app/src/features/ask/index.ts:
--------------------------------------------------------------------------------
1 | export { AskPage } from "./ui/ask";
2 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | run:
2 | ./devrun.sh
3 |
4 | fmt:
5 | cd backend && go fmt ./...
--------------------------------------------------------------------------------
/app/src/features/word/index.ts:
--------------------------------------------------------------------------------
1 | export { WordPage } from "./ui/word_page";
2 |
--------------------------------------------------------------------------------
/app/src/features/edit_card/index.ts:
--------------------------------------------------------------------------------
1 | export { EditPage } from "./ui/edit_page";
2 |
--------------------------------------------------------------------------------
/app/src/features/login/index.tsx:
--------------------------------------------------------------------------------
1 | export { LoginPage } from "./ui/login_page";
2 |
--------------------------------------------------------------------------------
/app/src/features/onboarding/index.ts:
--------------------------------------------------------------------------------
1 | export { Onboarding } from "./ui/main";
2 |
--------------------------------------------------------------------------------
/app/src/features/profile/index.ts:
--------------------------------------------------------------------------------
1 | export { ProfilePage } from "./ui/profile";
2 |
--------------------------------------------------------------------------------
/app/src/features/create_card/index.tsx:
--------------------------------------------------------------------------------
1 | export { CreateWordPage } from "./ui/main";
2 |
--------------------------------------------------------------------------------
/app/public/chinese.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/app/public/chinese.png
--------------------------------------------------------------------------------
/app/public/english.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/app/public/english.png
--------------------------------------------------------------------------------
/app/public/french.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/app/public/french.png
--------------------------------------------------------------------------------
/app/public/german.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/app/public/german.png
--------------------------------------------------------------------------------
/app/public/russian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/app/public/russian.png
--------------------------------------------------------------------------------
/app/public/turkish.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/app/public/turkish.png
--------------------------------------------------------------------------------
/landing/public/hero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/landing/public/hero.jpg
--------------------------------------------------------------------------------
/landing/public/en_paradox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/landing/public/en_paradox.png
--------------------------------------------------------------------------------
/landing/public/fr_paradox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/landing/public/fr_paradox.png
--------------------------------------------------------------------------------
/landing/public/ru_paradox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/landing/public/ru_paradox.png
--------------------------------------------------------------------------------
/landing/public/tr_paradox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/landing/public/tr_paradox.png
--------------------------------------------------------------------------------
/landing/public/zh_paradox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/woerter/main/landing/public/zh_paradox.png
--------------------------------------------------------------------------------
/app/src/features/home/index.ts:
--------------------------------------------------------------------------------
1 | export { Home } from "./ui/home";
2 | export { useCardStore } from "./store/card_store";
3 |
--------------------------------------------------------------------------------
/app/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/landing/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "jsxImportSource": "react"
6 | }
7 | }
--------------------------------------------------------------------------------
/app/src/features/common/index.tsx:
--------------------------------------------------------------------------------
1 | export { Header } from "./ui/header";
2 | export { AuthProvider } from "./provider/auth";
3 | export { useAuthStore } from "./store/auth";
4 |
--------------------------------------------------------------------------------
/app/src/features/play/index.ts:
--------------------------------------------------------------------------------
1 | export { PlayPage } from "./ui/play";
2 | export { GoPlayPage } from "./ui/goplay";
3 | export type { PlayType } from "./store/play_store";
4 |
--------------------------------------------------------------------------------
/app/src/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/utils/string.ts:
--------------------------------------------------------------------------------
1 | export function Capitalize(str: string) {
2 | if (typeof str !== "string" || str.length === 0) {
3 | return "";
4 | }
5 | return str.charAt(0).toUpperCase() + str.slice(1);
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/api/get_token.ts:
--------------------------------------------------------------------------------
1 | import { getCookieValue } from "@/utils/cookie";
2 |
3 | export function getToken() {
4 | const token = getCookieValue("Authorization");
5 | if (token === null) {
6 | throw new Error("No token");
7 | }
8 |
9 | return token;
10 | }
11 |
--------------------------------------------------------------------------------
/landing/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "astro/config";
2 | import tailwind from "@astrojs/tailwind";
3 |
4 | import react from "@astrojs/react";
5 |
6 | // https://astro.build/config
7 | export default defineConfig({
8 | integrations: [tailwind(), react()]
9 | });
--------------------------------------------------------------------------------
/app/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/features/international/index.ts:
--------------------------------------------------------------------------------
1 | export { useI8 } from "./hooks/use_i8";
2 | export type { OsLanguageValues } from "./assets/languages";
3 | export { I8Provider } from "./provider/i8_provider";
4 | export { OsLanguages, TargetLanguages } from "./assets/languages";
5 | export { ChangeLanguage } from "./ui/change_lang";
6 |
--------------------------------------------------------------------------------
/app/src/types/words.ts:
--------------------------------------------------------------------------------
1 | export type WordType = {
2 | id: string;
3 | title: string;
4 | description: string;
5 | created_at: string;
6 | updated_at: string;
7 | from_language: string;
8 | to_language: string;
9 | type: string;
10 | };
11 |
12 | export type CardType = {
13 | language: string;
14 | words: WordType[];
15 | };
16 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Woerter
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/landing/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 |
4 | # generated types
5 | .astro/
6 |
7 | # dependencies
8 | node_modules/
9 |
10 | # logs
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
23 | # jetbrains setting folder
24 | .idea/
25 |
--------------------------------------------------------------------------------
/landing/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
4 | theme: {
5 | container: {
6 | center: true,
7 | padding: "36px",
8 | screens: {
9 | "2xl": "1200px",
10 | },
11 | },
12 | extend: {},
13 | },
14 | plugins: [],
15 | };
16 |
--------------------------------------------------------------------------------
/app/src/types/user.ts:
--------------------------------------------------------------------------------
1 | export type StateType = "logged" | "loading" | "noinfo";
2 | export type ProfileType = {
3 | id: string;
4 | name: string;
5 | full_name: string;
6 | email: string;
7 | avatar: string;
8 | language: string;
9 | created_at: string;
10 | };
11 |
12 | export type LanguageType = {
13 | id: string;
14 | name: string;
15 | user_id: string;
16 | created_at: string;
17 | };
18 |
--------------------------------------------------------------------------------
/app/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/utils/cn"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/api/play.ts:
--------------------------------------------------------------------------------
1 | import { PlayType } from "@/features/play";
2 | import { getToken } from "./get_token";
3 |
4 | export async function GoPlayLoad(count: number, lang: string) {
5 | const token = getToken();
6 |
7 | const req = await fetch(`/api/v1/play?count=${count}&lang=${lang}`, {
8 | headers: {
9 | Authorization: token,
10 | },
11 | });
12 |
13 | if (!req.ok) throw new Error("Bad request");
14 |
15 | const data: PlayType = await req.json();
16 | return data;
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/utils/cookie.ts:
--------------------------------------------------------------------------------
1 | export function getCookieValue(key: string): string | null {
2 | const cookies = document.cookie.split(";");
3 |
4 | for (const cookie of cookies) {
5 | const [cookieKey, cookieValue] = cookie.trim().split("=");
6 |
7 | if (cookieKey === key) {
8 | return decodeURIComponent(cookieValue);
9 | }
10 | }
11 |
12 | return null;
13 | }
14 |
15 | export function deleteCookie(key: string): void {
16 | document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
17 | }
18 |
--------------------------------------------------------------------------------
/landing/src/components/Whyus.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | why: string;
4 | }
5 |
6 | const { why } = Astro.props;
7 | ---
8 |
9 |
12 |
13 |
{why}
14 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/landing/src/components/Whyus_card.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | title: string;
4 | desc: string;
5 | }
6 |
7 | const { title, desc } = Astro.props;
8 | ---
9 |
10 |
13 |
16 | {title}
17 |
18 |
19 |
20 | {desc}
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import "./index.css";
3 | import { RouterProvider } from "react-router-dom";
4 | import { router } from "@/routes";
5 |
6 | import { QueryClient, QueryClientProvider } from "react-query";
7 | import { I8Provider } from "./features/international";
8 |
9 | export const queryClient = new QueryClient();
10 |
11 | ReactDOM.createRoot(document.getElementById("root")!).render(
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/app/src/features/home/store/card_store.ts:
--------------------------------------------------------------------------------
1 | import { CardType } from "@/types/words";
2 | import { create } from "zustand";
3 |
4 | type ICards = {
5 | state: "loading" | "loaded";
6 | cards: CardType[];
7 | setCards: (newCards: CardType[]) => void;
8 | addCard: (newCard: CardType) => void;
9 | setLoaded: () => void;
10 | };
11 |
12 | export const useCardStore = create((set) => ({
13 | state: "loading",
14 | cards: [],
15 | setCards: (newCards) => set({ cards: newCards }),
16 | addCard: (newCard) => set((state) => ({ cards: [...state.cards, newCard] })),
17 | setLoaded: () => set({ state: "loaded" }),
18 | }));
19 |
--------------------------------------------------------------------------------
/landing/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | title: string;
4 | lang: string;
5 | }
6 |
7 | const { title, lang } = Astro.props;
8 | ---
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {title}
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/features/international/store/langs.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { LanguageText, Texts } from "../assets/all_text";
3 | import { OsLanguageValues } from "../assets/languages";
4 |
5 | type II8 = {
6 | currentLang: OsLanguageValues;
7 | text: LanguageText;
8 | setLang: (lang: OsLanguageValues) => void;
9 | };
10 |
11 | export const useI8Store = create((set) => ({
12 | currentLang: "english",
13 | text: Texts[0],
14 | setLang: (lang) =>
15 | set(() => {
16 | const newText = Texts.filter((text) => text.value === lang)[0];
17 | return { currentLang: lang, text: newText };
18 | }),
19 | }));
20 |
--------------------------------------------------------------------------------
/landing/src/icons/Trust.astro:
--------------------------------------------------------------------------------
1 | ---
2 | type Props = {
3 | size: string;
4 | strokeWidth: number;
5 | };
6 |
7 | const { size, strokeWidth } = Astro.props;
8 | ---
9 |
10 |
17 |
25 |
26 |
--------------------------------------------------------------------------------
/landing/src/components/CTA.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | title: string;
4 | desc1: string;
5 | button: string;
6 | }
7 |
8 | const { title, desc1, button } = Astro.props;
9 | ---
10 |
11 |
12 |
13 |
14 | {title}
15 |
16 | {desc1}
18 |
19 |
20 | {button}
24 |
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Общие игнорируемые файлы и папки
2 | node_modules/
3 | dist/
4 | build/
5 | bin/
6 | static/
7 | .env
8 | .TODO
9 | .instruction
10 | ins.txt
11 | .DS_Store
12 | .vscode/
13 | .idea/
14 | *.log
15 |
16 | # Игнорирование для Go (backend)
17 | backend/bin/
18 | backend/pkg/
19 | backend/.env
20 | backend/.DS_Store
21 | backend/.vscode/
22 | backend/.idea/
23 | backend/*.log
24 | backend/coverage/
25 | backend/test-results/
26 |
27 | # Игнорирование для React с использованием Vite (frontend)
28 | frontend/node_modules/
29 | frontend/dist/
30 | frontend/.env
31 | frontend/.DS_Store
32 | frontend/.vscode/
33 | frontend/.idea/
34 | frontend/*.log
35 | frontend/coverage/
36 | frontend/test-results/
37 |
--------------------------------------------------------------------------------
/landing/src/components/ChangeLang.tsx:
--------------------------------------------------------------------------------
1 | export function ChangeLang(props: { currectLang: string }) {
2 | function Change(value: string) {
3 | window.location.href = "/" + value;
4 | }
5 | return (
6 | Change(e.target.value)}
12 | >
13 | English
14 | Русский язык
15 | Français
16 | Türkçe
17 | 中文
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/features/play/store/play_store.ts:
--------------------------------------------------------------------------------
1 | import { WordType } from "@/types/words";
2 | import { create } from "zustand";
3 |
4 | export type PlayType = {
5 | count: string;
6 | words: WordType[];
7 | };
8 |
9 | type IPlay = {
10 | card: PlayType | null;
11 | step: number;
12 | setCard: (newCards: PlayType) => void;
13 | stepInc: () => void;
14 | stepDec: () => void;
15 | cleanCard: () => void;
16 | };
17 |
18 | export const usePlayStore = create((set) => ({
19 | card: null,
20 | step: 1,
21 | setCard: (newCards) => set({ card: newCards }),
22 | stepInc: () => set((state) => ({ step: state.step + 1 })),
23 | stepDec: () => set((state) => ({ step: state.step - 1 })),
24 | cleanCard: () => set({ card: null, step: 1 }),
25 | }));
26 |
--------------------------------------------------------------------------------
/app/src/api/me.ts:
--------------------------------------------------------------------------------
1 | import { LanguageType, ProfileType } from "@/types/user";
2 | import { getToken } from "./get_token";
3 |
4 | export async function GetMe(token: string) {
5 | const data = await fetch("/api/v1/me", {
6 | headers: {
7 | Authorization: token,
8 | },
9 | });
10 | const user: {
11 | user: ProfileType;
12 | languages: LanguageType[];
13 | } = await data.json();
14 | return user;
15 | }
16 |
17 | export function OnboardUpdate(body: {
18 | os_language: string;
19 | target_languages: string[];
20 | }) {
21 | const token = getToken();
22 | return fetch("/api/v1/onboard", {
23 | method: "PATCH",
24 | headers: {
25 | Authorization: token,
26 | },
27 | body: JSON.stringify(body),
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/landing/src/icons/Click.astro:
--------------------------------------------------------------------------------
1 | ---
2 | type Props = {
3 | size: string;
4 | strokeWidth: number;
5 | };
6 |
7 | const { size, strokeWidth } = Astro.props;
8 | ---
9 |
10 |
17 |
23 |
24 |
--------------------------------------------------------------------------------
/landing/src/components/Paradox.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | title: string;
4 | desc1: string;
5 | desc2: string;
6 | image: string;
7 | }
8 |
9 | const { title, desc1, desc2, image } = Astro.props;
10 | ---
11 |
12 |
13 |
14 |
15 |
{title}
16 |
17 | {desc1}
18 |
19 |
20 |
21 | {desc2}
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/landing/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/landing/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "landing",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro check && astro build",
9 | "preview": "astro preview",
10 | "astro": "astro"
11 | },
12 | "dependencies": {
13 | "@astrojs/check": "^0.7.0",
14 | "@astrojs/react": "^3.6.0",
15 | "@astrojs/tailwind": "^5.1.0",
16 | "@astropub/icons": "^0.2.0",
17 | "@types/react": "^18.3.3",
18 | "@types/react-dom": "^18.3.0",
19 | "astro": "^4.11.3",
20 | "path": "^0.12.7",
21 | "react": "^18.3.1",
22 | "react-dom": "^18.3.1",
23 | "tailwindcss": "^3.4.4",
24 | "typescript": "^5.5.2"
25 | },
26 | "devDependencies": {
27 | "astro-icon": "^1.1.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/eslint.config.js:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import pluginJs from "@eslint/js";
3 | import tseslint from "typescript-eslint";
4 | import pluginReactConfig from "eslint-plugin-react/configs/recommended.js";
5 | import { fixupConfigRules } from "@eslint/compat";
6 |
7 | export default [
8 | { files: ["**/*.js"], languageOptions: { sourceType: "script" } },
9 | { languageOptions: { globals: globals.browser } },
10 | pluginJs.configs.recommended,
11 | ...tseslint.configs.recommended,
12 | {
13 | files: ["**/*.jsx"],
14 | languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
15 | },
16 | ...fixupConfigRules(pluginReactConfig),
17 | {
18 | rules: {
19 | "react/react-in-jsx-scope": "off",
20 | "react/jsx-uses-react": "off",
21 | },
22 | },
23 | ];
24 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": [
6 | "ES2020",
7 | "DOM",
8 | "DOM.Iterable"
9 | ],
10 | "module": "ESNext",
11 | "skipLibCheck": true,
12 | /* Bundler mode */
13 | "moduleResolution": "bundler",
14 | "allowImportingTsExtensions": true,
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "paths": {
25 | "@/*": [
26 | "./src/*"
27 | ]
28 | }
29 | },
30 | "include": [
31 | "src"
32 | ],
33 | "references": [
34 | {
35 | "path": "./tsconfig.node.json"
36 | }
37 | ]
38 | }
--------------------------------------------------------------------------------
/landing/src/components/Header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { ChangeLang } from "./ChangeLang";
3 | interface Props {
4 | button: string;
5 | currentLang: string;
6 | }
7 | const { button, currentLang } = Astro.props;
8 | ---
9 |
10 |
11 |
12 |
SyncWord
14 |
SW
15 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/features/home/ui/home.tsx:
--------------------------------------------------------------------------------
1 | import { CardsList } from "./cards_list";
2 | import { Navigate, useLocation } from "react-router-dom";
3 | import { useAuthStore } from "@/features/common";
4 |
5 | function useQueryParams() {
6 | return new URLSearchParams(useLocation().search);
7 | }
8 |
9 | export function Home() {
10 | const { state, profile } = useAuthStore();
11 | const query = useQueryParams();
12 | if (
13 | state === "logged" &&
14 | profile.languages.length === 0 &&
15 | query.get("state") !== "done"
16 | ) {
17 | return ;
18 | }
19 | if (query.get("state") === "done") {
20 | window.location.href = "/app/";
21 | }
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/features/international/ui/change_lang.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Select,
3 | SelectContent,
4 | SelectItem,
5 | SelectTrigger,
6 | SelectValue,
7 | } from "@/components/ui/select";
8 | import { OsLanguages, useI8 } from "..";
9 |
10 | export function ChangeLanguage() {
11 | const { setLanguage, oslang } = useI8();
12 | return (
13 | {
16 | setLanguage(v);
17 | }}
18 | >
19 |
20 |
21 |
22 |
23 | {OsLanguages.map((lang) => {
24 | return (
25 |
26 | {lang.text}
27 |
28 | );
29 | })}
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # 1. Base Image with pnpm
2 | FROM node:20 as base
3 | WORKDIR /base
4 |
5 | RUN npm install -g pnpm
6 |
7 | # 2. Build App
8 | FROM base as builder-app
9 | WORKDIR /app
10 |
11 | COPY app/pnpm-lock.yaml app/package.json ./
12 | RUN pnpm install
13 |
14 | COPY app/ ./
15 | RUN pnpm run build
16 |
17 | # 3. Build Landing
18 | FROM base as builder-landing
19 | WORKDIR /landing
20 |
21 | COPY landing/pnpm-lock.yaml landing/package.json ./
22 | RUN pnpm install
23 |
24 | COPY landing/ ./
25 | RUN pnpm run build
26 |
27 | # 4. Nginx
28 | FROM nginx:alpine AS nginx
29 |
30 | COPY nginx.conf /etc/nginx/nginx.conf
31 | COPY --from=builder-app /app/dist /usr/share/nginx/html/app
32 | COPY --from=builder-landing /landing/dist /usr/share/nginx/html/landing
33 |
34 | # 5. Certbot (Let's Encrypt)
35 | FROM certbot/certbot AS certbot
36 |
37 | # Создаем необходимые директории для SSL сертификатов
38 | RUN mkdir -p /etc/letsencrypt
39 |
--------------------------------------------------------------------------------
/app/src/features/international/provider/i8_provider.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useLayoutEffect } from "react";
2 | import { useI8 } from "..";
3 | import { useAuthStore } from "@/features/common";
4 |
5 | function GetOSLang(state: string, userLanguage: string) {
6 | if (state !== "logged" || userLanguage === "") {
7 | const lsLang = localStorage.getItem("lang");
8 | if (lsLang !== null) {
9 | return lsLang;
10 | } else {
11 | return navigator.language.substring(0, 2);
12 | }
13 | } else if (state === "logged" && userLanguage !== "") {
14 | localStorage.setItem("lang", userLanguage);
15 | return userLanguage;
16 | }
17 | return navigator.language.substring(0, 2);
18 | }
19 |
20 | export function I8Provider(props: { children: ReactNode }) {
21 | const { state, profile } = useAuthStore();
22 | const { setLanguage } = useI8();
23 | useLayoutEffect(() => {
24 | setLanguage(GetOSLang(state, profile.user.language));
25 | }, [state, profile]);
26 | return props.children;
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/features/international/hooks/use_i8.ts:
--------------------------------------------------------------------------------
1 | import { OsLanguageValues } from "../assets/languages";
2 | import { useI8Store } from "../store/langs";
3 |
4 | export function useI8() {
5 | const { currentLang, text, setLang } = useI8Store();
6 | function setLanguage(lang: string) {
7 | setLang(Convert(lang));
8 | }
9 | return {
10 | oslang: currentLang,
11 | t: text.t,
12 | setLanguage,
13 | };
14 | }
15 |
16 | function Convert(lang: string) {
17 | let oslang: OsLanguageValues = "english";
18 | switch (lang) {
19 | case "en":
20 | case "english":
21 | oslang = "english";
22 | break;
23 | case "ru":
24 | case "russian":
25 | oslang = "russian";
26 | break;
27 | case "fr":
28 | case "french":
29 | oslang = "french";
30 | break;
31 | case "tr":
32 | case "turkish":
33 | oslang = "turkish";
34 | break;
35 | case "zh":
36 | case "chinese":
37 | oslang = "chinese";
38 | break;
39 | }
40 | return oslang;
41 | }
42 |
--------------------------------------------------------------------------------
/app/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import path from "path";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | base: "/app",
8 | build: {
9 | outDir: "./dist",
10 | },
11 | plugins: [react()],
12 | resolve: {
13 | alias: {
14 | "@": path.resolve(__dirname, "./src"),
15 | },
16 | },
17 | server: {
18 | proxy: {
19 | "/api": {
20 | target: "http://localhost:8081",
21 | changeOrigin: true,
22 | secure: false,
23 | },
24 | "/oauth": {
25 | target: "http://localhost:8081",
26 | changeOrigin: true,
27 | secure: false,
28 | },
29 | "/metrics": {
30 | target: "http://localhost:8081",
31 | changeOrigin: true,
32 | secure: false,
33 | },
34 | "^/(?!app).*": {
35 | target: "http://localhost:4321",
36 | changeOrigin: true,
37 | secure: false,
38 | },
39 | },
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/app/src/features/common/provider/auth.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useEffect } from "react";
2 | import { Navigate } from "react-router-dom";
3 | import { getCookieValue, deleteCookie } from "@/utils/cookie";
4 | import { useAuthStore } from "../store/auth";
5 | import { GetMe } from "@/api/me";
6 |
7 | type AuthProviderProps = {
8 | children: ReactNode;
9 | };
10 |
11 | export function AuthProvider(props: AuthProviderProps) {
12 | const { changeState, changeProfile, state } = useAuthStore();
13 |
14 | async function Initial() {
15 | const token = getCookieValue("Authorization");
16 | if (token === null) {
17 | changeState("noinfo");
18 | return;
19 | }
20 | try {
21 | const user = await GetMe(token);
22 | changeProfile(user);
23 | changeState("logged");
24 | } catch (error) {
25 | changeState("noinfo");
26 | deleteCookie("Authorization");
27 | }
28 | }
29 | useEffect(() => {
30 | Initial();
31 | }, []);
32 |
33 | if (state === "noinfo") {
34 | return ;
35 | }
36 |
37 | return props.children;
38 | }
39 |
--------------------------------------------------------------------------------
/landing/src/components/Hero.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | login: string;
4 | try: string;
5 | }
6 | const { login, try: trying } = Astro.props;
7 | ---
8 |
9 |
12 |
13 |
27 |
31 |
32 |
33 |
43 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | nginx:
5 | build:
6 | context: .
7 | target: nginx
8 | container_name: nginx
9 | ports:
10 | - "80:80"
11 | - "443:443"
12 | volumes:
13 | - ./nginx.conf:/etc/nginx/nginx.conf
14 | - /etc/letsencrypt:/etc/letsencrypt # Монтируем папку с сертификатами
15 |
16 | certbot:
17 | image: certbot/certbot
18 | container_name: certbot
19 | volumes:
20 | - /etc/letsencrypt:/etc/letsencrypt
21 | - /var/lib/letsencrypt:/var/lib/letsencrypt
22 | - /var/log/letsencrypt:/var/log/letsencrypt
23 | entrypoint: /bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'
24 |
25 | go_app:
26 | image: heilethan/yerd:0.0.15
27 | container_name: go_app
28 | ports:
29 | - "8081:8081"
30 | volumes:
31 | - ./main.db:/app/main.db
32 | - ./log.db:/app/log.db
33 |
34 | metric_app:
35 | image: heilethan/swadmin:0.0.3
36 | container_name: metric_app
37 | ports:
38 | - "3000:3000"
39 | volumes:
40 | - /home/erdanaerboluly/woerter/main.db:/app/main.db
41 |
--------------------------------------------------------------------------------
/app/src/features/international/assets/languages.ts:
--------------------------------------------------------------------------------
1 | export const TargetLanguages = [
2 | {
3 | value: "english",
4 | text: "English",
5 | icon: "english.png",
6 | },
7 | {
8 | value: "german",
9 | text: "Deutsch",
10 | icon: "german.png",
11 | },
12 | ];
13 |
14 | export const OsLanguages: Language[] = [
15 | {
16 | value: "russian",
17 | short: "RU",
18 | text: "Русский язык",
19 | icon: "russian.png",
20 | },
21 | {
22 | value: "english",
23 | short: "EN",
24 | text: "English",
25 | icon: "english.png",
26 | },
27 | {
28 | value: "french",
29 | short: "FR",
30 | text: "Français",
31 | icon: "french.png",
32 | },
33 | {
34 | value: "turkish",
35 | short: "TR",
36 | text: "Türkçe",
37 | icon: "turkish.png",
38 | },
39 | {
40 | value: "chinese",
41 | short: "ZH",
42 | text: "中文",
43 | icon: "chinese.png",
44 | },
45 | ];
46 |
47 | export type Language = {
48 | value: string;
49 | short: string;
50 | text: string;
51 | icon: string;
52 | };
53 |
54 | export type OsLanguageValues =
55 | | "russian"
56 | | "english"
57 | | "french"
58 | | "turkish"
59 | | "chinese";
60 |
--------------------------------------------------------------------------------
/app/src/features/common/store/auth.ts:
--------------------------------------------------------------------------------
1 | import { LanguageType, ProfileType, StateType } from "@/types/user";
2 | import { create } from "zustand";
3 |
4 | type IAuth = {
5 | state: StateType;
6 | changeState: (newState: StateType) => void;
7 | profile: {
8 | user: ProfileType;
9 | languages: LanguageType[];
10 | };
11 | changeProfile: (newProfile: {
12 | user: ProfileType;
13 | languages: LanguageType[];
14 | }) => void;
15 | cleanUser: () => void;
16 | };
17 |
18 | export const useAuthStore = create((set) => ({
19 | state: "loading",
20 | changeState: (newState: StateType) => set({ state: newState }),
21 | profile: {
22 | user: {
23 | id: "",
24 | name: "",
25 | full_name: "",
26 | email: "",
27 | avatar: "",
28 | language: "",
29 | created_at: "",
30 | },
31 | languages: [],
32 | },
33 | changeProfile: (newProfile: {
34 | user: ProfileType;
35 | languages: LanguageType[];
36 | }) => set({ profile: newProfile }),
37 | cleanUser: () =>
38 | set({
39 | profile: {
40 | user: {
41 | id: "",
42 | name: "",
43 | full_name: "",
44 | email: "",
45 | avatar: "",
46 | language: "",
47 | created_at: "",
48 | },
49 | languages: [],
50 | },
51 | state: "noinfo",
52 | }),
53 | }));
54 |
--------------------------------------------------------------------------------
/landing/src/icons/Info.astro:
--------------------------------------------------------------------------------
1 | ---
2 | type Props = {
3 | size: string;
4 | strokeWidth: number;
5 | };
6 |
7 | const { size, strokeWidth } = Astro.props;
8 | ---
9 |
10 |
18 | info-feed
19 |
26 |
27 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/devrun.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Функция для завершения запущенных процессов
4 | cleanup() {
5 | echo "Завершаем процессы..."
6 | kill $VITE_PID $ASTRO_PID 2>/dev/null
7 | exit 0
8 | }
9 |
10 | # Устанавливаем обработчик для сигнала SIGINT (Ctrl+C)
11 | trap cleanup SIGINT
12 |
13 | # Перейти в папку app и запустить Vite React проект в режиме разработки
14 | cd app || { echo "Не удалось перейти в папку app"; exit 1; }
15 | echo "Запуск Vite React проекта в папке app..."
16 | pnpm run dev &
17 |
18 | # Сохранить PID Vite процесса, чтобы в случае необходимости можно было его остановить
19 | VITE_PID=$!
20 |
21 | # Проверить успешный запуск Vite
22 | if [ $? -ne 0 ]; then
23 | echo "Ошибка запуска Vite React проекта в папке app"
24 | exit 1
25 | fi
26 |
27 | # Вернуться в корневую директорию
28 | cd ..
29 |
30 | # Перейти в папку landing и запустить Astro проект в режиме разработки
31 | cd landing || { echo "Не удалось перейти в папку landing"; exit 1; }
32 | echo "Запуск Astro проекта в папке landing..."
33 | pnpm run dev &
34 |
35 | # Сохранить PID Astro процесса, чтобы в случае необходимости можно было его остановить
36 | ASTRO_PID=$!
37 |
38 | # Проверить успешный запуск Astro
39 | if [ $? -ne 0 ]; then
40 | echo "Ошибка запуска Astro проекта в папке landing"
41 | exit 1
42 | fi
43 |
44 | echo "Оба проекта запущены успешно!"
45 | echo "Vite PID: $VITE_PID, Astro PID: $ASTRO_PID"
46 |
47 | # Ожидание завершения обоих процессов
48 | wait $VITE_PID $ASTRO_PID
49 |
--------------------------------------------------------------------------------
/app/src/features/word/ui/word_page.tsx:
--------------------------------------------------------------------------------
1 | import { GetWord } from "@/api/word";
2 | import { useEffect, useState } from "react";
3 | import { Link, useNavigate, useParams } from "react-router-dom";
4 | import { Pencil } from "lucide-react";
5 | import { WordType } from "@/types/words";
6 |
7 | export function WordPage() {
8 | const { id } = useParams();
9 | const navigate = useNavigate();
10 | const [word, setWord] = useState(null);
11 | async function Initial() {
12 | try {
13 | if (id !== undefined) {
14 | const word = await GetWord(id);
15 | setWord(word);
16 | }
17 | } catch (error) {
18 | console.log(error);
19 | navigate("/app/");
20 | }
21 | }
22 | useEffect(() => {
23 | Initial();
24 | }, []);
25 | return (
26 | <>
27 |
28 | {word && (
29 | <>
30 |
34 |
35 |
36 |
37 | {word.title}
38 |
39 |
40 | {word.description}
41 |
42 | >
43 | )}
44 |
45 | >
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@radix-ui/react-dialog": "^1.1.1",
14 | "@radix-ui/react-dropdown-menu": "^2.1.1",
15 | "@radix-ui/react-select": "^2.1.1",
16 | "@uidotdev/usehooks": "^2.4.1",
17 | "class-variance-authority": "^0.7.0",
18 | "clsx": "^2.1.1",
19 | "lucide-react": "^0.395.0",
20 | "path": "^0.12.7",
21 | "react": "^18.2.0",
22 | "react-dom": "^18.2.0",
23 | "react-query": "^3.39.3",
24 | "react-router-dom": "^6.23.1",
25 | "tailwind-merge": "^2.3.0",
26 | "tailwindcss-animate": "^1.0.7",
27 | "uuid": "^10.0.0",
28 | "zustand": "^4.5.2"
29 | },
30 | "devDependencies": {
31 | "@eslint/compat": "^1.1.0",
32 | "@eslint/js": "^9.4.0",
33 | "@types/node": "^20.14.2",
34 | "@types/react": "^18.2.66",
35 | "@types/react-dom": "^18.2.22",
36 | "@types/uuid": "^9.0.8",
37 | "@typescript-eslint/eslint-plugin": "^7.2.0",
38 | "@typescript-eslint/parser": "^7.2.0",
39 | "@vitejs/plugin-react": "^4.2.1",
40 | "autoprefixer": "^10.4.19",
41 | "eslint": "^9.4.0",
42 | "eslint-plugin-react": "^7.34.2",
43 | "eslint-plugin-react-hooks": "^4.6.0",
44 | "eslint-plugin-react-refresh": "^0.4.6",
45 | "globals": "^15.4.0",
46 | "postcss": "^8.4.38",
47 | "tailwindcss": "^3.4.4",
48 | "typescript": "^5.2.2",
49 | "typescript-eslint": "^7.13.0",
50 | "vite": "^5.2.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet, createBrowserRouter } from "react-router-dom";
2 | import { Home } from "@/features/home";
3 | import { CreateWordPage } from "@/features/create_card";
4 | import { WordPage } from "@/features/word";
5 | import { EditPage } from "@/features/edit_card";
6 | import { Onboarding } from "@/features/onboarding";
7 | import { AskPage } from "@/features/ask";
8 | import { GoPlayPage, PlayPage } from "@/features/play";
9 | import { ProfilePage } from "@/features/profile";
10 | import { LoginPage } from "@/features/login";
11 | import { AuthProvider, Header } from "./features/common";
12 |
13 | export const router = createBrowserRouter([
14 | {
15 | path: "/app/login",
16 | element: ,
17 | },
18 | {
19 | path: "/",
20 | element: (
21 |
22 |
23 |
24 |
25 | ),
26 | children: [
27 | {
28 | path: "/app",
29 | element: ,
30 | },
31 | {
32 | path: "/app/play",
33 | element: ,
34 | },
35 | {
36 | path: "/app/profile",
37 | element: ,
38 | },
39 | {
40 | path: "/app/goplay",
41 | element: ,
42 | },
43 | {
44 | path: "/app/create/:lang",
45 | element: ,
46 | },
47 | {
48 | path: "/app/dic/:id",
49 | element: ,
50 | },
51 | {
52 | path: "/app/edit/:id",
53 | element: ,
54 | },
55 | {
56 | path: "/app/onboard",
57 | element: ,
58 | },
59 | {
60 | path: "/app/ask/:lang",
61 | element: ,
62 | },
63 | ],
64 | },
65 | ]);
66 |
--------------------------------------------------------------------------------
/landing/src/icons/Repeat.astro:
--------------------------------------------------------------------------------
1 | ---
2 | type Props = {
3 | size: string;
4 | strokeWidth: number;
5 | };
6 |
7 | const { size, strokeWidth } = Astro.props;
8 | ---
9 |
10 |
17 |
20 |
23 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 240 10% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 240 10% 3.9%;
15 |
16 | --primary: 240 5.9% 10%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 240 4.8% 95.9%;
20 | --secondary-foreground: 240 5.9% 10%;
21 |
22 | --muted: 240 4.8% 95.9%;
23 | --muted-foreground: 240 3.8% 46.1%;
24 |
25 | --accent: 240 4.8% 95.9%;
26 | --accent-foreground: 240 5.9% 10%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 240 5.9% 90%;
32 | --input: 240 5.9% 90%;
33 | --ring: 240 10% 3.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 240 10% 3.9%;
40 | --foreground: 0 0% 98%;
41 |
42 | --card: 240 10% 3.9%;
43 | --card-foreground: 0 0% 98%;
44 |
45 | --popover: 240 10% 3.9%;
46 | --popover-foreground: 0 0% 98%;
47 |
48 | --primary: 0 0% 98%;
49 | --primary-foreground: 240 5.9% 10%;
50 |
51 | --secondary: 240 3.7% 15.9%;
52 | --secondary-foreground: 0 0% 98%;
53 |
54 | --muted: 240 3.7% 15.9%;
55 | --muted-foreground: 240 5% 64.9%;
56 |
57 | --accent: 240 3.7% 15.9%;
58 | --accent-foreground: 0 0% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 0 0% 98%;
62 |
63 | --border: 240 3.7% 15.9%;
64 | --input: 240 3.7% 15.9%;
65 | --ring: 240 4.9% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
77 |
78 | main{
79 | padding-bottom: 100px !important;
80 | }
--------------------------------------------------------------------------------
/app/src/api/word.ts:
--------------------------------------------------------------------------------
1 | import { CardType, WordType } from "@/types/words";
2 | import { getToken } from "./get_token";
3 |
4 | export async function CreateWord(word: WordType) {
5 | const token = getToken();
6 | const req = await fetch("/api/v1/word", {
7 | method: "POST",
8 | headers: {
9 | Authorization: token,
10 | },
11 | body: JSON.stringify(word),
12 | });
13 | if (!req.ok) throw new Error("word not created");
14 | const data: CardType = await req.json();
15 | return data;
16 | }
17 |
18 | export async function UpdateWord(word: WordType) {
19 | const token = getToken();
20 | const req = await fetch("/api/v1/word", {
21 | method: "PATCH",
22 | headers: {
23 | Authorization: token,
24 | },
25 | body: JSON.stringify(word),
26 | });
27 | if (!req.ok) throw new Error("word not updated");
28 | return req;
29 | }
30 |
31 | export async function GetAllWord() {
32 | const token = getToken();
33 | const req = await fetch("/api/v1/word", {
34 | headers: {
35 | Authorization: token,
36 | },
37 | });
38 | const data: CardType[] = await req.json();
39 | if (!req.ok) throw new Error("words not found");
40 | return data;
41 | }
42 |
43 | export async function GetSearchWordResult(prefix: string) {
44 | const token = getToken();
45 | console.log("first");
46 | const req = await fetch(`/api/v1/searchword?prefix=${prefix}`, {
47 | headers: {
48 | Authorization: token,
49 | },
50 | });
51 | const data: WordType[] = await req.json();
52 | if (!req.ok) throw new Error("words not found");
53 | return data;
54 | }
55 |
56 | export async function GetWord(id: string) {
57 | const token = getToken();
58 | const req = await fetch(`/api/v1/word/${id}`, {
59 | headers: {
60 | Authorization: token,
61 | },
62 | });
63 | if (!req.ok) throw new Error("words not found");
64 | const data: WordType = await req.json();
65 |
66 | return data;
67 | }
68 |
69 | export async function DeleteWord(id: string) {
70 | const token = getToken();
71 | const req = await fetch(`/api/v1/word/${id}`, {
72 | method: "DELETE",
73 | headers: {
74 | Authorization: token,
75 | },
76 | });
77 | if (!req.ok) throw new Error("words not found");
78 | return req;
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/features/play/ui/goplay.tsx:
--------------------------------------------------------------------------------
1 | import { useI8 } from "@/features/international";
2 | import { usePlayStore } from "../store/play_store";
3 | import { useNavigate } from "react-router-dom";
4 | import { Navigate } from "react-router-dom";
5 |
6 | export function GoPlayPage() {
7 | const { step, card, stepInc, stepDec, cleanCard } = usePlayStore();
8 | const { t } = useI8();
9 | const navigate = useNavigate();
10 | if (card === null) {
11 | return ;
12 | }
13 |
14 | return (
15 |
16 |
17 |
22 | {t.GOPLAY.BACK}
23 |
24 |
25 | {step} {" "}
26 | / {card?.words.length}
27 |
28 | {card !== null && step < card?.words.length ? (
29 | = card?.words.length}
31 | onClick={stepInc}
32 | className="py-3 px-5 text-sm bg-purple-600 disabled:bg-purple-400 disabled:cursor-not-allowed rounded-xl text-white"
33 | >
34 | {t.GOPLAY.NEXT}
35 |
36 | ) : (
37 | {
40 | cleanCard();
41 | navigate("/app/play");
42 | }}
43 | >
44 | {t.GOPLAY.END}
45 |
46 | )}
47 |
48 |
49 |
50 | {card?.words[step - 1].title}
51 |
52 |
53 | {card?.words[step - 1].description}
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/features/international/assets/cn_text.ts:
--------------------------------------------------------------------------------
1 | import { LanguageText } from "./all_text";
2 |
3 | export const ZH_TEXT: LanguageText = {
4 | value: "chinese",
5 | t: {
6 | WORD: {
7 | CREATE: "添加",
8 | SHOW_FULL: "显示全部",
9 | EMPTY1: "这里是空的,",
10 | EMPTY2: "添加新单词 🤪",
11 | },
12 | PLAY: {
13 | H1_P1: "让我们",
14 | H1_P2: "学习!",
15 | MAX: "复习单词数(最多50个)",
16 | GO: "开始吧!",
17 | },
18 | ONBOARD: {
19 | THIS_H1: "选择您的主要语言:",
20 | THIS_P1: `选择您流利使用的语言。所有界面和单词描述都将使用此语言。您每30天只能更改一次语言。`,
21 | THIS_B1: "保存",
22 | ANOTHER_H1: "选择要学习的语言:",
23 | ANOTHER_P1: "仅选择您真正想学习的语言。",
24 | ANOTHER_P2: "您还没有选择任何语言。",
25 | ANOTHER_P3: "您已选择:",
26 | ANOTHER_B1: "保存",
27 | },
28 | ASK: {
29 | B1: "生成",
30 | B2: "前往单词",
31 | SELF: "我自己写",
32 | YOUR_LANG: "用 ",
33 | INPUT_P: "保持简短清晰 ;)",
34 | G_M: "如何获得好结果?",
35 | G1: "- 用以下语言写单词:",
36 | G2: "- 写单词时不做语法变化",
37 | G3: "- 如果可能,只写单词或短语",
38 | },
39 | LOGIN: {
40 | WELCOME: "欢迎!",
41 | NEXT: "请使用 Google 登录继续 👀",
42 | GOOGLE: "使用 Google 登录",
43 | },
44 | CREATE: {
45 | P_EN: `用英语解释单词 "[[]]" 的意思。答案需要用英语写。首先,解释这个词的基本意思。然后用英语造3个句子并提供翻译。具体解释每个句子的上下文中的单词含义。写得简明扼要,不要有不必要的信息。答案需要没有 Markdown 格式。`,
46 | P_DE: `用英语解释单词 "[[]]" 的意思。答案需要用英语写。首先,解释这个词的基本意思。然后用德语造3个句子并提供翻译。具体解释每个句子的上下文中的单词含义。写得简明扼要,不要有不必要的信息。答案需要没有 Markdown 格式。`,
47 | TITLE: "您的单词:",
48 | TITLE_P: "保持简短清晰 ;)",
49 | DESC: "这是什么意思:",
50 | DESC_P: "为您的单词写一个描述:",
51 | SELF: "我想自己做",
52 | GEN: "生成",
53 | GEN1: "1. 复制提示:",
54 | GEN2_P1: "2. 访问网站 ",
55 | GEN2_P2: " 并粘贴文本",
56 | GEN3: "3. 复制结果并粘贴到描述字段",
57 | SAVE: "保存",
58 | },
59 | EDIT: {
60 | TITLE: "您的单词:",
61 | TITLE_P: "保持简短清晰 ;)",
62 | DESC: "这是什么意思:",
63 | DESC_P: "为您的单词写一个描述:",
64 | DELETE: "删除",
65 | SAVE: "保存",
66 | },
67 | GOPLAY: {
68 | END: "结束",
69 | NEXT: "下一步",
70 | BACK: "返回",
71 | },
72 | HEADER: {
73 | WORDS: "单词",
74 | PLAY: "游戏",
75 | PROFILE: "个人资料",
76 | HI: "你好,",
77 | NEW: "新单词",
78 | CHOOSE: "选择语言",
79 | },
80 | PROFILE: {
81 | TLANGS: "您正在学习这些语言:",
82 | OSLANG: "系统语言:",
83 | LOGOUT: "登出",
84 | },
85 | },
86 | };
87 |
--------------------------------------------------------------------------------
/app/src/features/international/assets/all_text.ts:
--------------------------------------------------------------------------------
1 | import { EN_TEXT } from "./en_text";
2 | import { OsLanguageValues } from "./languages";
3 | import { RU_TEXT } from "./ru_text";
4 | import { FR_TEXT } from "./fr_text";
5 | import { TR_TEXT } from "./tr_text";
6 | import { ZH_TEXT } from "./cn_text";
7 |
8 | export type LanguageText = {
9 | value: OsLanguageValues;
10 | t: {
11 | WORD: {
12 | CREATE: string;
13 | SHOW_FULL: string;
14 | EMPTY1: string;
15 | EMPTY2: string;
16 | };
17 | PLAY: {
18 | H1_P1: string;
19 | H1_P2: string;
20 | MAX: string;
21 | GO: string;
22 | };
23 | ONBOARD: {
24 | THIS_H1: string;
25 | THIS_P1: string;
26 | THIS_B1: string;
27 | ANOTHER_H1: string;
28 | ANOTHER_P1: string;
29 | ANOTHER_P2: string;
30 | ANOTHER_P3: string;
31 | ANOTHER_B1: string;
32 | };
33 | ASK: {
34 | B1: string;
35 | B2: string;
36 | SELF: string;
37 | YOUR_LANG: string;
38 | INPUT_P: string;
39 | G_M: string;
40 | G1: string;
41 | G2: string;
42 | G3: string;
43 | };
44 | LOGIN: {
45 | WELCOME: string;
46 | NEXT: string;
47 | GOOGLE: string;
48 | };
49 | CREATE: {
50 | P_EN: string;
51 | P_DE: string;
52 | TITLE: string;
53 | TITLE_P: string;
54 | DESC: string;
55 | DESC_P: string;
56 | SELF: string;
57 | GEN: string;
58 | GEN1: string;
59 | GEN2_P1: string;
60 | GEN2_P2: string;
61 | GEN3: string;
62 | SAVE: string;
63 | };
64 | EDIT: {
65 | TITLE: string;
66 | TITLE_P: string;
67 | DESC: string;
68 | DESC_P: string;
69 | DELETE: string;
70 | SAVE: string;
71 | };
72 | GOPLAY: {
73 | END: string;
74 | NEXT: string;
75 | BACK: string;
76 | };
77 | HEADER: {
78 | WORDS: string;
79 | PLAY: string;
80 | PROFILE: string;
81 | HI: string;
82 | NEW: string;
83 | CHOOSE: string;
84 | };
85 | PROFILE: {
86 | TLANGS: string;
87 | OSLANG: string;
88 | LOGOUT: string;
89 | };
90 | };
91 | };
92 |
93 | export const Texts: LanguageText[] = [
94 | RU_TEXT,
95 | EN_TEXT,
96 | FR_TEXT,
97 | TR_TEXT,
98 | ZH_TEXT,
99 | ];
100 |
--------------------------------------------------------------------------------
/app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{ts,tsx}",
6 | "./components/**/*.{ts,tsx}",
7 | "./app/**/*.{ts,tsx}",
8 | "./src/**/*.{ts,tsx}",
9 | ],
10 | prefix: "",
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: "24px",
15 | screens: {
16 | "2xl": "1200px",
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | keyframes: {
61 | "accordion-down": {
62 | from: { height: "0" },
63 | to: { height: "var(--radix-accordion-content-height)" },
64 | },
65 | "accordion-up": {
66 | from: { height: "var(--radix-accordion-content-height)" },
67 | to: { height: "0" },
68 | },
69 | },
70 | animation: {
71 | "accordion-down": "accordion-down 0.2s ease-out",
72 | "accordion-up": "accordion-up 0.2s ease-out",
73 | },
74 | },
75 | },
76 | plugins: [require("tailwindcss-animate")],
77 | };
78 |
--------------------------------------------------------------------------------
/landing/README.md:
--------------------------------------------------------------------------------
1 | # Astro Starter Kit: Basics
2 |
3 | ```sh
4 | npm create astro@latest -- --template basics
5 | ```
6 |
7 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
8 | [](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
9 | [](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
10 |
11 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
12 |
13 | 
14 |
15 | ## 🚀 Project Structure
16 |
17 | Inside of your Astro project, you'll see the following folders and files:
18 |
19 | ```text
20 | /
21 | ├── public/
22 | │ └── favicon.svg
23 | ├── src/
24 | │ ├── components/
25 | │ │ └── Card.astro
26 | │ ├── layouts/
27 | │ │ └── Layout.astro
28 | │ └── pages/
29 | │ └── index.astro
30 | └── package.json
31 | ```
32 |
33 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
34 |
35 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
36 |
37 | Any static assets, like images, can be placed in the `public/` directory.
38 |
39 | ## 🧞 Commands
40 |
41 | All commands are run from the root of the project, from a terminal:
42 |
43 | | Command | Action |
44 | | :------------------------ | :----------------------------------------------- |
45 | | `npm install` | Installs dependencies |
46 | | `npm run dev` | Starts local dev server at `localhost:4321` |
47 | | `npm run build` | Build your production site to `./dist/` |
48 | | `npm run preview` | Preview your build locally, before deploying |
49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
50 | | `npm run astro -- --help` | Get help using the Astro CLI |
51 |
52 | ## 👀 Want to learn more?
53 |
54 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
55 |
--------------------------------------------------------------------------------
/app/src/features/profile/ui/profile.tsx:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from "@/features/common";
2 | import { useI8 } from "@/features/international";
3 | import { deleteCookie } from "@/utils/cookie";
4 | import { Capitalize } from "@/utils/string";
5 | import { useNavigate } from "react-router-dom";
6 |
7 | export function ProfilePage() {
8 | const { profile, cleanUser } = useAuthStore();
9 | const navigate = useNavigate();
10 | const { t } = useI8();
11 | function Logout() {
12 | deleteCookie("Authorization");
13 | cleanUser();
14 | navigate("/app");
15 | }
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | {profile.user.full_name}
23 |
24 |
25 | {profile.user.email}
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 | {t.PROFILE.TLANGS}
38 |
39 |
40 | {profile.languages.map((lang) => {
41 | return (
42 |
46 | {Capitalize(lang.name)}
47 |
48 | );
49 | })}
50 |
51 |
52 |
53 |
54 | {t.PROFILE.OSLANG}
55 |
56 | {Capitalize(profile.user.language)}
57 |
58 |
59 |
60 |
64 | {t.PROFILE.LOGOUT}
65 |
66 | {/*
*/}
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/features/login/ui/login_page.tsx:
--------------------------------------------------------------------------------
1 | import { useI8 } from "@/features/international";
2 | import { getCookieValue } from "@/utils/cookie";
3 | import { Navigate } from "react-router-dom";
4 |
5 | export function LoginPage() {
6 | const { t } = useI8();
7 | const token = getCookieValue("Authorization");
8 | if (token !== null && token !== "") {
9 | return ;
10 | }
11 | return (
12 |
13 |
30 |
31 | );
32 | }
33 |
34 | export function GoogleLogo() {
35 | return (
36 |
43 |
47 |
51 |
55 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/landing/src/pages/zh/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Header from "../../components/Header.astro";
3 | import Hero from "../../components/Hero.astro";
4 | import Paradox from "../../components/Paradox.astro";
5 | import Whyus from "../../components/Whyus.astro";
6 | import Layout from "../../layouts/Layout.astro";
7 | import CTA from "../../components/CTA.astro";
8 | import Footer from "../../components/Footer.astro";
9 | import WhyusCard from "../../components/Whyus_card.astro";
10 | import Click from "../../icons/Click.astro";
11 | import Info from "../../icons/Info.astro";
12 | import Trust from "../../icons/Trust.astro";
13 | import Repeat from "../../icons/Repeat.astro";
14 | ---
15 |
16 |
17 |
18 |
19 |
20 | 更快掌握新
21 |
22 | 词汇
23 |
26 | 更快
27 |
28 |
29 |
30 | Syncword 使用 人工智能 来
37 |
38 | 让语言学习更轻松
39 |
40 |
41 |
47 |
48 |
49 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
67 |
68 |
69 |
73 |
74 |
75 |
76 |
77 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app/src/features/home/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import { useI8 } from "@/features/international";
2 |
3 | import { Link } from "react-router-dom";
4 | import { Eye } from "lucide-react";
5 | import { CardType, WordType } from "@/types/words";
6 |
7 | export function Card(props: { card: CardType }) {
8 | const { t } = useI8();
9 | if (props.card && props.card.words.length !== 0) {
10 | return (
11 |
12 | {props.card.words.map((word, i) => {
13 | return ;
14 | })}
15 |
16 | );
17 | }
18 | return (
19 |
20 |
21 | {t.WORD.EMPTY1}
22 |
23 | {t.WORD.EMPTY2}
24 |
25 |
26 | );
27 | }
28 |
29 | export function CardLoading() {
30 | return (
31 |
32 | {[...new Array(10)].map((_, i) => {
33 | return (
34 |
38 |
39 |
40 | Some title
41 |
42 |
43 | Some description
44 |
45 | Some description
46 |
47 | Some description
48 |
49 |
50 |
51 | Some text
52 |
53 |
54 | );
55 | })}
56 |
57 | );
58 | }
59 |
60 | function Word(props: { word: WordType }) {
61 | const { t } = useI8();
62 | return (
63 |
64 |
65 |
66 | {props.word.title}
67 |
68 |
69 | {props.word.description}
70 |
71 |
72 |
76 |
77 | {t.WORD.SHOW_FULL}
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | events {}
2 |
3 | http {
4 | include mime.types;
5 | default_type text/html;
6 |
7 | # Глобальная настройка MIME-типов для JS модулей
8 | types {
9 | application/javascript js;
10 | application/json json;
11 | text/css css;
12 | }
13 |
14 | # Редирект HTTP на HTTPS
15 | server {
16 | listen 80;
17 | server_name ethanapp.de www.ethanapp.de;
18 | return 301 https://$host$request_uri;
19 | }
20 |
21 | # Сервер для основного домена с SSL
22 | server {
23 | listen 443 ssl;
24 | server_name ethanapp.de www.ethanapp.de;
25 |
26 | ssl_certificate /etc/letsencrypt/live/ethanapp.de/fullchain.pem;
27 | ssl_certificate_key /etc/letsencrypt/live/ethanapp.de/privkey.pem;
28 |
29 | # Конфигурация для лендинга в корне сайта (/)
30 | location / {
31 | root /usr/share/nginx/html/landing;
32 | index index.html;
33 | try_files $uri $uri/ /index.html;
34 | }
35 |
36 | # Конфигурация для React-приложения в /app
37 | location /app {
38 | alias /usr/share/nginx/html/app/;
39 | try_files $uri $uri/ /app/index.html;
40 | }
41 |
42 | # Проксировать API запросы на Go сервер
43 | location /api/ {
44 | proxy_pass http://go_app:8081;
45 | proxy_set_header Host $host;
46 | proxy_set_header X-Real-IP $remote_addr;
47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
48 | proxy_set_header X-Forwarded-Proto $scheme;
49 | }
50 |
51 | # Проксировать OAUTH запросы на Go сервер
52 | location /oauth/ {
53 | proxy_pass http://go_app:8081;
54 | proxy_set_header Host $host;
55 | proxy_set_header X-Real-IP $remote_addr;
56 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
57 | proxy_set_header X-Forwarded-Proto $scheme;
58 | }
59 |
60 | location /metrics {
61 | proxy_pass http://metric_app:3000;
62 | proxy_set_header Host $host;
63 | proxy_set_header X-Real-IP $remote_addr;
64 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
65 | proxy_set_header X-Forwarded-Proto $scheme;
66 | }
67 |
68 | location /api/v1/ask {
69 | proxy_pass http://go_app:8081;
70 | proxy_http_version 1.1;
71 | proxy_set_header Upgrade $http_upgrade;
72 | proxy_set_header Connection "upgrade";
73 | proxy_set_header Host $host;
74 | proxy_cache_bypass $http_upgrade;
75 | proxy_set_header X-Real-IP $remote_addr;
76 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
77 | proxy_set_header X-Forwarded-Proto $scheme;
78 |
79 | # Включение поддержки стриминга
80 | proxy_buffering off;
81 | proxy_cache off;
82 | chunked_transfer_encoding off;
83 | }
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/app/src/features/edit_card/ui/edit_page.tsx:
--------------------------------------------------------------------------------
1 | import { DeleteWord, GetWord, UpdateWord } from "@/api/word";
2 | import { useI8 } from "@/features/international";
3 | import { WordType } from "@/types/words";
4 | import { useEffect, useState } from "react";
5 | import { useNavigate, useParams } from "react-router-dom";
6 |
7 | export function EditPage() {
8 | //TODO: доделать
9 | const { id } = useParams();
10 | const { t } = useI8();
11 | const [title, setTitle] = useState("");
12 | const [desc, setDesc] = useState("");
13 | const navigate = useNavigate();
14 | const [word, setWord] = useState(null);
15 |
16 | async function Update() {
17 | if (word !== null) {
18 | try {
19 | const newWord = word;
20 | newWord.title = title;
21 | newWord.description = desc;
22 | await UpdateWord(newWord);
23 | navigate("/app/dic/" + id);
24 | } catch (error) {
25 | console.log(error);
26 | }
27 | }
28 | }
29 |
30 | async function Delete() {
31 | if (word !== null) {
32 | try {
33 | await DeleteWord(word.id);
34 | navigate("/app");
35 | } catch (error) {
36 | console.log(error);
37 | }
38 | }
39 | }
40 |
41 | async function Initial() {
42 | if (id !== undefined) {
43 | try {
44 | const word = await GetWord(id);
45 | setWord(word);
46 | setTitle(word.title);
47 | setDesc(word.description);
48 | } catch (error) {
49 | console.log(error);
50 | }
51 | } else {
52 | navigate("/app");
53 | }
54 | }
55 | useEffect(() => {
56 | Initial();
57 | }, []);
58 | return (
59 |
60 |
61 | {t.EDIT.TITLE}
62 | setTitle(e.target.value)}
66 | placeholder={t.EDIT.TITLE_P}
67 | className="border border-zinc-300 rounded-2xl pl-6 py-3 focus:outline focus:outline-purple-500"
68 | />
69 |
70 |
71 | {t.EDIT.DESC}
72 |
73 |
81 |
82 |
86 | {t.EDIT.DELETE}
87 |
88 |
92 | {t.EDIT.SAVE}
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/app/src/features/home/ui/cards_list.tsx:
--------------------------------------------------------------------------------
1 | import { useCardStore } from "..";
2 | import { Capitalize } from "@/utils/string";
3 | import { useEffect, useState } from "react";
4 | import { cn } from "@/utils/cn";
5 | import { GetAllWord } from "@/api/word";
6 | import { Card, CardLoading } from "./card";
7 | import { CardType } from "@/types/words";
8 | import { useAuthStore } from "@/features/common";
9 | import { SearchWord } from "./search";
10 |
11 | export function CardsList() {
12 | const { cards, setCards, state, setLoaded } = useCardStore();
13 | const { profile, state: userState } = useAuthStore();
14 | const [currentTarget, setCurrentTarget] = useState("");
15 |
16 | async function Initial() {
17 | try {
18 | const data = await GetAllWord();
19 | setCards(data);
20 | setLoaded();
21 | } catch (error) {
22 | console.log(error);
23 | }
24 | }
25 |
26 | useEffect(() => {
27 | Initial();
28 | }, []);
29 |
30 | useEffect(() => {
31 | if (userState === "logged") {
32 | setCurrentTarget(profile.languages[0].name);
33 | }
34 | }, [userState]);
35 |
36 | return (
37 |
38 |
39 | {state === "loaded" ? (
40 | setCurrentTarget(value)}
44 | />
45 | ) : (
46 |
47 | )}
48 |
49 | {state === "loaded" ? (
50 |
card.language === currentTarget)!} />
51 | ) : (
52 |
53 | )}
54 |
55 | );
56 | }
57 |
58 | function LanguageChange(props: {
59 | cards: CardType[];
60 | currentTarget: string;
61 | setTarget: (value: string) => void;
62 | }) {
63 | return (
64 |
65 |
66 | {props.cards.map((card) => {
67 | return (
68 | props.setTarget(card.language)}
70 | key={card.language}
71 | className={cn("py-2 px-4 rounded-t-lg text-zinc-400 text-sm", {
72 | "border-b-4 border-b-purple-600 text-zinc-800 font-medium":
73 | props.currentTarget === card.language,
74 | })}
75 | >
76 | {Capitalize(card.language)}
77 |
78 | );
79 | })}
80 |
81 |
82 |
83 | );
84 | }
85 |
86 | function LanguageChangeLoading() {
87 | return (
88 |
89 |
90 | Somete
91 |
92 |
93 | Somete
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/app/src/features/home/ui/search.tsx:
--------------------------------------------------------------------------------
1 | import { GetSearchWordResult } from "@/api/word";
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | DialogTrigger,
8 | } from "@/components/ui/dialog";
9 | import { WordType } from "@/types/words";
10 | import { ChevronRight, Search } from "lucide-react";
11 | import { useEffect, useState } from "react";
12 | import { useDebounce } from "@uidotdev/usehooks";
13 | import { Link } from "react-router-dom";
14 |
15 | export function SearchWord() {
16 | const [searchTerm, setSearchTerm] = useState("");
17 | const [results, setResults] = useState([]);
18 | const debouncedSearchTerm = useDebounce(searchTerm, 500);
19 |
20 | function handleChange(value: string) {
21 | setSearchTerm(value);
22 | }
23 |
24 | useEffect(() => {
25 | (async () => {
26 | if (debouncedSearchTerm) {
27 | try {
28 | const data = await GetSearchWordResult(debouncedSearchTerm);
29 | setResults(data);
30 | } catch (error) {
31 | console.log(error);
32 | }
33 | } else {
34 | setResults([]);
35 | }
36 | })();
37 | }, [debouncedSearchTerm]);
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | Search a word
48 |
49 |
50 |
51 |
55 | handleChange(e.target.value)}
61 | />
62 |
63 |
64 | Found : {results.length}
65 |
66 |
67 | {results.map((word) => {
68 | return ;
69 | })}
70 |
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | function SearchWordCard(props: { word: WordType }) {
78 | return (
79 |
80 |
81 |
82 | {props.word.to_language}
83 |
84 | {props.word.title}
85 |
86 |
90 |
91 |
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/landing/src/components/Footer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | links: string;
4 | }
5 |
6 | const { links } = Astro.props;
7 | ---
8 |
9 |
80 |
--------------------------------------------------------------------------------
/landing/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Header from "../components/Header.astro";
3 | import Hero from "../components/Hero.astro";
4 | import Paradox from "../components/Paradox.astro";
5 | import Whyus from "../components/Whyus.astro";
6 | import Layout from "../layouts/Layout.astro";
7 | import CTA from "../components/CTA.astro";
8 | import Footer from "../components/Footer.astro";
9 | import WhyusCard from "../components/Whyus_card.astro";
10 | import Click from "../icons/Click.astro";
11 | import Info from "../icons/Info.astro";
12 | import Trust from "../icons/Trust.astro";
13 | import Repeat from "../icons/Repeat.astro";
14 | ---
15 |
16 |
17 |
18 |
19 |
20 | Master new
21 |
22 | words
23 |
26 | faster
27 |
28 |
29 |
30 | Syncword uses AI to
37 |
38 | make language learning easier
39 |
40 |
41 |
47 |
48 |
49 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
67 |
68 |
69 |
73 |
74 |
75 |
76 |
77 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/landing/src/pages/ru/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Header from "../../components/Header.astro";
3 | import Hero from "../../components/Hero.astro";
4 | import Paradox from "../../components/Paradox.astro";
5 | import Whyus from "../../components/Whyus.astro";
6 | import Layout from "../../layouts/Layout.astro";
7 | import CTA from "../../components/CTA.astro";
8 | import Footer from "../../components/Footer.astro";
9 | import WhyusCard from "../../components/Whyus_card.astro";
10 | import Click from "../../icons/Click.astro";
11 | import Info from "../../icons/Info.astro";
12 | import Trust from "../../icons/Trust.astro";
13 | import Repeat from "../../icons/Repeat.astro";
14 | ---
15 |
16 |
17 |
18 |
19 |
20 | Осваивай новые
21 |
22 | слова
23 |
26 | быстрее
27 |
28 |
29 |
30 | Syncword использует ИИ для
37 |
38 | облегчения изучения языка
39 |
40 |
41 |
47 |
48 |
49 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
67 |
68 |
69 |
73 |
74 |
75 |
76 |
77 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/landing/src/pages/en/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Header from "../../components/Header.astro";
3 | import Hero from "../../components/Hero.astro";
4 | import Paradox from "../../components/Paradox.astro";
5 | import Whyus from "../../components/Whyus.astro";
6 | import Layout from "../../layouts/Layout.astro";
7 | import CTA from "../../components/CTA.astro";
8 | import Footer from "../../components/Footer.astro";
9 | import WhyusCard from "../../components/Whyus_card.astro";
10 | import Click from "../../icons/Click.astro";
11 | import Info from "../../icons/Info.astro";
12 | import Trust from "../../icons/Trust.astro";
13 | import Repeat from "../../icons/Repeat.astro";
14 | ---
15 |
16 |
17 |
18 |
19 |
20 | Master new
21 |
22 | words
23 |
26 | faster
27 |
28 |
29 |
30 | Syncword uses AI to
37 |
38 | make language learning easier
39 |
40 |
41 |
47 |
48 |
49 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
67 |
68 |
69 |
73 |
74 |
75 |
76 |
77 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/landing/src/pages/tr/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Header from "../../components/Header.astro";
3 | import Hero from "../../components/Hero.astro";
4 | import Paradox from "../../components/Paradox.astro";
5 | import Whyus from "../../components/Whyus.astro";
6 | import Layout from "../../layouts/Layout.astro";
7 | import CTA from "../../components/CTA.astro";
8 | import Footer from "../../components/Footer.astro";
9 | import WhyusCard from "../../components/Whyus_card.astro";
10 | import Click from "../../icons/Click.astro";
11 | import Info from "../../icons/Info.astro";
12 | import Trust from "../../icons/Trust.astro";
13 | import Repeat from "../../icons/Repeat.astro";
14 | ---
15 |
16 |
17 |
18 |
19 |
20 | Yeni kelimeleri
21 |
22 | öğrenin
23 |
26 | daha hızlı
27 |
28 |
29 |
30 | Syncword
34 | Yapay Zeka kullanarak
38 |
39 | dil öğrenimini kolaylaştırır
40 |
41 |
42 |
48 |
49 |
50 |
54 |
55 |
56 |
60 |
61 |
62 |
63 |
64 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/app/src/features/international/assets/ru_text.ts:
--------------------------------------------------------------------------------
1 | import { LanguageText } from "./all_text";
2 |
3 | export const RU_TEXT: LanguageText = {
4 | value: "russian",
5 | t: {
6 | WORD: {
7 | CREATE: "Добавить",
8 | SHOW_FULL: "Показать все",
9 | EMPTY1: "Здесь пока пусто,",
10 | EMPTY2: "добавь новые слова 🤪",
11 | },
12 | PLAY: {
13 | H1_P1: "Давай",
14 | H1_P2: "учиться!",
15 | MAX: "сколько слов хочешь повторить (максимум 50)",
16 | GO: "Поехали!",
17 | },
18 | ONBOARD: {
19 | THIS_H1: "Выбери основной язык:",
20 | THIS_P1: `Выбери язык, на котором ты говоришь свободно. Все интерфейсы
21 | приложения и описания слов будут на этом языке. Менять
22 | язык можно будет только раз в 30 дней.`,
23 | THIS_B1: "Сохранить",
24 | ANOTHER_H1: "Выбери языки для изучения:",
25 | ANOTHER_P1:
26 | "Отметь только те языки, которые действительно хочешь изучать.",
27 | ANOTHER_P2: "Ты пока ничего не выбрал.",
28 | ANOTHER_P3: "Ты выбрал: ",
29 | ANOTHER_B1: "Сохранить",
30 | },
31 | ASK: {
32 | B1: "Сгенерировать",
33 | B2: "Перейти к слову",
34 | SELF: "Напишу сам",
35 | YOUR_LANG: "Напиши на ",
36 | INPUT_P: "Напиши коротко и ясно ;)",
37 | G_M: "Как получить лучший результат?",
38 | G1: "- Напиши слово на языке: ",
39 | G2: "- Не изменяй грамматику слова",
40 | G3: "- Пиши только слово или короткую фразу",
41 | },
42 | LOGIN: {
43 | WELCOME: "Добро пожаловать!",
44 | NEXT: "Пожалуйста, войди через Google, чтобы продолжить 👀",
45 | GOOGLE: "Войти с помощью Google",
46 | },
47 | CREATE: {
48 | P_EN: `Объясни, что означает слово "[[]]" на английском.
49 | Ответ нужен на русском. Сначала дай общее объяснение слова.
50 | Затем составь 3 предложения на английском и переведи их на русский. Объясни
51 | значение слова в каждом предложении. Напиши коротко и ясно,
52 | без лишней информации. Ответ нужен без Markdown-разметки.`,
53 | P_DE: `Объясни, что означает слово "[[]]" на немецком.
54 | Ответ нужен на русском. Сначала дай общее объяснение слова.
55 | Затем составь 3 предложения на немецком и переведи их на русский. Объясни
56 | значение слова в каждом предложении. Напиши коротко и ясно,
57 | без лишней информации. Ответ нужен без Markdown-разметки.`,
58 | TITLE: "Твое слово:",
59 | TITLE_P: "Напиши коротко и ясно ;)",
60 | DESC: "Что это значит:",
61 | DESC_P: "Напиши описание для своего слова:",
62 | SELF: "Хочу сам",
63 | GEN: "Сгенерировать",
64 | GEN1: "1. Скопируй текст:",
65 | GEN2_P1: "2. Перейди на сайт ",
66 | GEN2_P2: " и вставь текст",
67 | GEN3: "3. Скопируй результат и вставь его в поле описания",
68 | SAVE: "Сохранить",
69 | },
70 | EDIT: {
71 | TITLE: "Твое слово:",
72 | TITLE_P: "Напиши коротко и ясно ;)",
73 | DESC: "Что это значит:",
74 | DESC_P: "Напиши описание для своего слова:",
75 | DELETE: "Удалить",
76 | SAVE: "Сохранить",
77 | },
78 | GOPLAY: {
79 | END: "Закончить",
80 | NEXT: "Дальше",
81 | BACK: "Назад",
82 | },
83 | HEADER: {
84 | WORDS: "Слова",
85 | PLAY: "Игра",
86 | PROFILE: "Профиль",
87 | HI: "Привет, ",
88 | NEW: "Новое слово",
89 | CHOOSE: "Выбери язык",
90 | },
91 | PROFILE: {
92 | TLANGS: "Ты изучаешь эти языки:",
93 | OSLANG: "Язык интерфейса:",
94 | LOGOUT: "Выйти",
95 | },
96 | },
97 | };
98 |
--------------------------------------------------------------------------------
/app/src/features/international/assets/tr_text.ts:
--------------------------------------------------------------------------------
1 | import { LanguageText } from "./all_text";
2 |
3 | export const TR_TEXT: LanguageText = {
4 | value: "turkish",
5 | t: {
6 | WORD: {
7 | CREATE: "Ekle",
8 | SHOW_FULL: "Hepsini göster",
9 | EMPTY1: "Burada henüz bir şey yok,",
10 | EMPTY2: "yeni kelimeler ekle 🤪",
11 | },
12 | PLAY: {
13 | H1_P1: "Haydi",
14 | H1_P2: "öğrenelim!",
15 | MAX: "gözden geçirmek istediğin kelimeler (en fazla 50)",
16 | GO: "Başlayalım!",
17 | },
18 | ONBOARD: {
19 | THIS_H1: "Ana dilini seç:",
20 | THIS_P1: `Akıcı konuştuğun dili seç. Tüm arayüzler ve kelime açıklamaları bu dilde olacak.
21 | Dili sadece 30 günde bir değiştirebilirsin.`,
22 | THIS_B1: "Kaydet",
23 | ANOTHER_H1: "Öğrenmek istediğin dilleri seç:",
24 | ANOTHER_P1: "Gerçekten öğrenmek istediğin dilleri seç.",
25 | ANOTHER_P2: "Henüz hiçbir şey seçmedin.",
26 | ANOTHER_P3: "Seçtiğin diller: ",
27 | ANOTHER_B1: "Kaydet",
28 | },
29 | ASK: {
30 | B1: "Oluştur",
31 | B2: "Kelimeye git",
32 | SELF: "Kendim yazacağım",
33 | YOUR_LANG: "Yaz dili: ",
34 | INPUT_P: "Kısa ve net yaz ;)",
35 | G_M: "İyi bir sonuç nasıl elde edilir?",
36 | G1: "- Kelimeyi şu dilde yaz: ",
37 | G2: "- Kelimeyi dilbilgisi değişiklikleri yapmadan yaz",
38 | G3: "- Mümkünse sadece kelimeyi veya kısa bir ifadeyi yaz",
39 | },
40 | LOGIN: {
41 | WELCOME: "Hoş geldin!",
42 | NEXT: "Devam etmek için lütfen Google ile giriş yap 👀",
43 | GOOGLE: "Google ile giriş yap",
44 | },
45 | CREATE: {
46 | P_EN: `İngilizce "[[[]]" kelimesinin ne anlama geldiğini açıklayınız.
47 | Cevabın Türkçe olarak verilmesi gerekmektedir. Önce kelimenin genel bir açıklamasını yapın.
48 | Daha sonra İngilizce 3 cümle kurun ve bunları Türkçeye çevirin. Açıklayınız
49 | Her bir cümledeki kelimenin anlamı. Kısa ve net yazın,
50 | gereksiz bilgi olmadan. Markdown işaretlemesi olmadan cevaba ihtiyaç vardır.`,
51 | P_DE: `Almanca'da "[[[]]" kelimesinin ne anlama geldiğini açıklayın.
52 | Cevaba Türkçe olarak ihtiyaç vardır. Önce kelimenin genel bir açıklamasını yapınız.
53 | Daha sonra Almanca 3 cümle kurunuz ve bunları Türkçeye çeviriniz. Açıklayınız
54 | Her bir cümledeki kelimenin anlamı. Kısa ve net yazın,
55 | gereksiz bilgi olmadan. Cevap, Markdown işaretlemesi olmadan gereklidir`,
56 | TITLE: "Senin kelimen:",
57 | TITLE_P: "Kısa ve net yaz ;)",
58 | DESC: "Bu ne anlama geliyor:",
59 | DESC_P: "Kelimen için bir açıklama yaz:",
60 | SELF: "Kendim yapmak istiyorum",
61 | GEN: "Oluştur",
62 | GEN1: "1. Metni kopyala:",
63 | GEN2_P1: "2. Siteye git ",
64 | GEN2_P2: " ve metni yapıştır",
65 | GEN3: "3. Sonucu kopyala ve açıklama alanına yapıştır",
66 | SAVE: "Kaydet",
67 | },
68 | EDIT: {
69 | TITLE: "Senin kelimen:",
70 | TITLE_P: "Kısa ve net yaz ;)",
71 | DESC: "Bu ne anlama geliyor:",
72 | DESC_P: "Kelimen için bir açıklama yaz:",
73 | DELETE: "Sil",
74 | SAVE: "Kaydet",
75 | },
76 | GOPLAY: {
77 | END: "Bitir",
78 | NEXT: "Sonraki",
79 | BACK: "Geri",
80 | },
81 | HEADER: {
82 | WORDS: "Kelimeler",
83 | PLAY: "Oyun",
84 | PROFILE: "Profil",
85 | HI: "Merhaba, ",
86 | NEW: "Yeni kelime",
87 | CHOOSE: "Dil seç",
88 | },
89 | PROFILE: {
90 | TLANGS: "Bu dilleri öğreniyorsun:",
91 | OSLANG: "Sistem dili:",
92 | LOGOUT: "Çıkış yap",
93 | },
94 | },
95 | };
96 |
--------------------------------------------------------------------------------
/landing/src/pages/fr/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Header from "../../components/Header.astro";
3 | import Hero from "../../components/Hero.astro";
4 | import Paradox from "../../components/Paradox.astro";
5 | import Whyus from "../../components/Whyus.astro";
6 | import Layout from "../../layouts/Layout.astro";
7 | import CTA from "../../components/CTA.astro";
8 | import Footer from "../../components/Footer.astro";
9 | import WhyusCard from "../../components/Whyus_card.astro";
10 | import Click from "../../icons/Click.astro";
11 | import Info from "../../icons/Info.astro";
12 | import Trust from "../../icons/Trust.astro";
13 | import Repeat from "../../icons/Repeat.astro";
14 | ---
15 |
16 |
17 |
18 |
19 |
20 | Maîtrisez de nouveaux
21 |
22 | mots
23 |
26 | plus rapidement
27 |
28 |
29 |
30 | Syncword utilise l'IA pour
37 |
38 | faciliter l'apprentissage des langues
39 |
40 |
41 |
47 |
48 |
49 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
67 |
68 |
69 |
73 |
74 |
75 |
76 |
77 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app/src/features/international/assets/en_text.ts:
--------------------------------------------------------------------------------
1 | import { LanguageText } from "./all_text";
2 |
3 | export const EN_TEXT: LanguageText = {
4 | value: "english",
5 | t: {
6 | WORD: {
7 | CREATE: "Add",
8 | SHOW_FULL: "Show all",
9 | EMPTY1: "It's empty here,",
10 | EMPTY2: "add new words 🤪",
11 | },
12 | PLAY: {
13 | H1_P1: "Let's",
14 | H1_P2: "learn!",
15 | MAX: "words to review (up to 50)",
16 | GO: "Let's go!",
17 | },
18 | ONBOARD: {
19 | THIS_H1: "Select your primary language:",
20 | THIS_P1: `Choose the language you speak fluently. All interfaces
21 | and word descriptions will be in this language. You can only change
22 | the language once every 30 days.`,
23 | THIS_B1: "Save",
24 | ANOTHER_H1: "Select languages to learn:",
25 | ANOTHER_P1: "Choose only the languages you really want to study.",
26 | ANOTHER_P2: "You haven't selected anything yet.",
27 | ANOTHER_P3: "You have selected: ",
28 | ANOTHER_B1: "Save",
29 | },
30 | ASK: {
31 | B1: "Generate",
32 | B2: "Go to word",
33 | SELF: "I'll write it myself",
34 | YOUR_LANG: "Write in ",
35 | INPUT_P: "Keep it short and clear ;)",
36 | G_M: "How to get a good result?",
37 | G1: "- Write the word in the language: ",
38 | G2: "- Write the word without grammatical changes",
39 | G3: "- If possible, write only the word or a short phrase",
40 | },
41 | LOGIN: {
42 | WELCOME: "Welcome!",
43 | NEXT: "Please log in with Google to continue 👀",
44 | GOOGLE: "Log in with Google",
45 | },
46 | CREATE: {
47 | P_EN: `Explain to me what the word "[[]]" means in English. The answer is needed in English.
48 | First, explain in general what this word means. Then make 3 sentences in English and provide
49 | the translation in English. Explain the meaning of the word specifically in the context of
50 | each sentence. Write concisely and clearly, without unnecessary information. The answer is
51 | needed without Markdown formatting.`,
52 | P_DE: `Explain to me what the word "[[]]" means in German. The answer is needed in English.
53 | First, explain in general what this word means. Then make 3 sentences in German and provide
54 | the translation in English. Explain the meaning of the word specifically in the context of
55 | each sentence. Write concisely and clearly, without unnecessary information. The answer is
56 | needed without Markdown formatting.`,
57 | TITLE: "Your word:",
58 | TITLE_P: "Keep it short and clear ;)",
59 | DESC: "What does it mean:",
60 | DESC_P: "Write a description for your word:",
61 | SELF: "I want to do it myself",
62 | GEN: "Generate",
63 | GEN1: "1. Copy the prompt:",
64 | GEN2_P1: "2. Go to the site ",
65 | GEN2_P2: " and paste the text",
66 | GEN3: "3. Copy the result and paste it into the description field",
67 | SAVE: "Save",
68 | },
69 | EDIT: {
70 | TITLE: "Your word:",
71 | TITLE_P: "Keep it short and clear ;)",
72 | DESC: "What does it mean:",
73 | DESC_P: "Write a description for your word:",
74 | DELETE: "Delete",
75 | SAVE: "Save",
76 | },
77 | GOPLAY: {
78 | END: "Finish",
79 | NEXT: "Next",
80 | BACK: "Back",
81 | },
82 | HEADER: {
83 | WORDS: "Words",
84 | PLAY: "Game",
85 | PROFILE: "Profile",
86 | HI: "Hi, ",
87 | NEW: "New word",
88 | CHOOSE: "Choose language",
89 | },
90 | PROFILE: {
91 | TLANGS: "You're learning these languages:",
92 | OSLANG: "System language:",
93 | LOGOUT: "Log out",
94 | },
95 | },
96 | };
97 |
--------------------------------------------------------------------------------
/app/src/features/international/assets/fr_text.ts:
--------------------------------------------------------------------------------
1 | import { LanguageText } from "./all_text";
2 |
3 | export const FR_TEXT: LanguageText = {
4 | value: "french",
5 | t: {
6 | WORD: {
7 | CREATE: "Ajouter",
8 | SHOW_FULL: "Tout afficher",
9 | EMPTY1: "C'est vide ici,",
10 | EMPTY2: "ajoute de nouveaux mots 🤪",
11 | },
12 | PLAY: {
13 | H1_P1: "Allons",
14 | H1_P2: "apprendre!",
15 | MAX: "mots à réviser (jusqu'à 50)",
16 | GO: "C'est parti!",
17 | },
18 | ONBOARD: {
19 | THIS_H1: "Choisis ta langue principale :",
20 | THIS_P1: `Choisis la langue que tu parles couramment. Toutes les interfaces
21 | et descriptions des mots seront dans cette langue. Tu pourras changer
22 | la langue une seule fois tous les 30 jours.`,
23 | THIS_B1: "Enregistrer",
24 | ANOTHER_H1: "Choisis les langues à apprendre :",
25 | ANOTHER_P1:
26 | "Sélectionne seulement les langues que tu veux vraiment étudier.",
27 | ANOTHER_P2: "Tu n'as encore rien choisi.",
28 | ANOTHER_P3: "Tu as choisi : ",
29 | ANOTHER_B1: "Enregistrer",
30 | },
31 | ASK: {
32 | B1: "Générer",
33 | B2: "Aller au mot",
34 | SELF: "Je vais écrire moi-même",
35 | YOUR_LANG: "Écris en ",
36 | INPUT_P: "Écris court et clair ;)",
37 | G_M: "Comment obtenir un bon résultat ?",
38 | G1: "- Écris le mot dans la langue : ",
39 | G2: "- Écris le mot sans modifications grammaticales",
40 | G3: "- Si possible, écris seulement le mot ou une courte phrase",
41 | },
42 | LOGIN: {
43 | WELCOME: "Bienvenue!",
44 | NEXT: "Veuillez vous connecter avec Google pour continuer 👀",
45 | GOOGLE: "Se connecter avec Google",
46 | },
47 | CREATE: {
48 | P_EN: `Expliquez ce que signifie le mot "[[[]]" en anglais.
49 | La réponse doit être donnée en français. Donnez d'abord une explication générale du mot.
50 | Ensuite, faites 3 phrases en anglais et traduisez-les en français. Expliquez
51 | le sens du mot dans chaque phrase. Rédigez des textes courts et clairs,
52 | sans informations inutiles. La réponse est nécessaire sans balisage Markdown.`,
53 | P_DE: `Expliquez la signification du mot "[[[]]" en allemand.
54 | La réponse est nécessaire en français. Donnez d'abord une explication générale du mot.
55 | Ensuite, faites 3 phrases en allemand et traduisez-les en français. Expliquez
56 | le sens du mot dans chaque phrase. Rédigez des textes courts et clairs,
57 | sans informations inutiles. La réponse est requise sans balisage Markdown`,
58 | TITLE: "Ton mot :",
59 | TITLE_P: "Écris court et clair ;)",
60 | DESC: "Que signifie-t-il :",
61 | DESC_P: "Écris une description pour ton mot :",
62 | SELF: "Je veux le faire moi-même",
63 | GEN: "Générer",
64 | GEN1: "1. Copie le texte :",
65 | GEN2_P1: "2. Va sur le site ",
66 | GEN2_P2: " et colle le texte",
67 | GEN3: "3. Copie le résultat et colle-le dans le champ de description",
68 | SAVE: "Enregistrer",
69 | },
70 | EDIT: {
71 | TITLE: "Ton mot :",
72 | TITLE_P: "Écris court et clair ;)",
73 | DESC: "Que signifie-t-il :",
74 | DESC_P: "Écris une description pour ton mot :",
75 | DELETE: "Supprimer",
76 | SAVE: "Enregistrer",
77 | },
78 | GOPLAY: {
79 | END: "Terminer",
80 | NEXT: "Suivant",
81 | BACK: "Retour",
82 | },
83 | HEADER: {
84 | WORDS: "Mots",
85 | PLAY: "Jeu",
86 | PROFILE: "Profil",
87 | HI: "Salut, ",
88 | NEW: "Nouveau mot",
89 | CHOOSE: "Choisir la langue",
90 | },
91 | PROFILE: {
92 | TLANGS: "Tu apprends ces langues :",
93 | OSLANG: "Langue du système :",
94 | LOGOUT: "Déconnexion",
95 | },
96 | },
97 | };
98 |
--------------------------------------------------------------------------------
/app/src/features/play/ui/play.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import {
3 | Select,
4 | SelectContent,
5 | SelectItem,
6 | SelectTrigger,
7 | SelectValue,
8 | } from "@/components/ui/select";
9 | import { Capitalize } from "@/utils/string";
10 | import { usePlayStore } from "../store/play_store";
11 | import { GoPlayLoad } from "@/api/play";
12 | import { Navigate, useNavigate } from "react-router-dom";
13 | import { useI8 } from "@/features/international";
14 | import { useAuthStore } from "@/features/common";
15 |
16 | export function PlayPage() {
17 | const { t } = useI8();
18 | const [count, setCount] = useState(10);
19 | const [lang, setLang] = useState("");
20 | const { profile } = useAuthStore();
21 | const { setCard, card } = usePlayStore();
22 | const navigate = useNavigate();
23 | async function LoadData() {
24 | try {
25 | const data = await GoPlayLoad(count, lang);
26 | setCard(data);
27 | navigate("/app/goplay");
28 | } catch (error) {}
29 | }
30 |
31 | if (card !== null) {
32 | return ;
33 | }
34 |
35 | function InputValue(value: string) {
36 | const num = Number(value);
37 | if (!isNaN(num)) {
38 | if (num >= 0 && num <= 50) setCount(num);
39 | }
40 | }
41 |
42 | useEffect(() => {
43 | if (profile.languages[0]) {
44 | setLang(profile.languages[0].name);
45 | }
46 | }, [profile]);
47 | //TODO: Добавить выбор языка, по нему автоматический count, Добавить примерную времю который займет повтор
48 | return (
49 |
50 |
51 | {t.PLAY.H1_P1}
52 |
53 |
54 | {t.PLAY.H1_P2}
55 |
56 |
57 |
58 |
59 | {
61 | if (count > 10) {
62 | setCount((prev) => prev - 1);
63 | }
64 | }}
65 | className="py-4 w-1/3 select-none flex justify-center items-center cursor-pointer"
66 | >
67 | -
68 |
69 | InputValue(e.target.value)}
72 | className="py-4 w-1/3 flex text-center focus:outline-purple-500 justify-center items-center cursor-pointer "
73 | />
74 |
75 | {
77 | if (count < 50) {
78 | setCount((prev) => prev + 1);
79 | }
80 | }}
81 | className="py-4 select-none w-1/3 flex justify-center items-center cursor-pointer"
82 | >
83 | +
84 |
85 |
86 |
{t.PLAY.MAX}
87 |
88 | setLang(value)}>
89 |
90 |
91 |
92 |
93 | {profile.languages.map((language) => (
94 |
95 | {Capitalize(language.name)}
96 |
97 | ))}
98 |
99 |
100 | 50}
103 | className="bg-gradient-to-br mt-12 from-indigo-400 to-indigo-600 rounded-lg disabled:from-zinc-300 disabled:to-zinc-400 disabled:cursor-not-allowed text-white py-4 px-8"
104 | >
105 | {t.PLAY.GO}
106 |
107 |
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/app/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { X } from "lucide-react"
4 |
5 | import { cn } from "@/utils/cn"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ))
52 | DialogContent.displayName = DialogPrimitive.Content.displayName
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | )
66 | DialogHeader.displayName = "DialogHeader"
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | )
80 | DialogFooter.displayName = "DialogFooter"
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogClose,
114 | DialogTrigger,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | }
121 |
--------------------------------------------------------------------------------
/app/src/features/ask/ui/ask.tsx:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from "@/features/common";
2 | import { useI8 } from "@/features/international";
3 | import { getCookieValue } from "@/utils/cookie";
4 | import { Capitalize } from "@/utils/string";
5 | import { ChevronDown, ChevronUp } from "lucide-react";
6 | import { useState } from "react";
7 | import { Link, Navigate, useParams } from "react-router-dom";
8 | import { v4 as uuidv4 } from "uuid";
9 |
10 | export function AskPage() {
11 | const { lang } = useParams();
12 | const { t } = useI8();
13 | const [msgs, setMsgs] = useState([]);
14 | const [word, setWord] = useState("");
15 | const { profile } = useAuthStore();
16 | const [requested, setRequested] = useState(false);
17 | const [wordId, setWordID] = useState("");
18 | const [isLoading, setLoading] = useState(false);
19 | if (
20 | lang === undefined ||
21 | !profile.languages
22 | .map((language) => {
23 | return language.name;
24 | })
25 | .includes(lang)
26 | ) {
27 | return ;
28 | }
29 | async function Getdata() {
30 | if (isLoading) return;
31 | setLoading(true);
32 | const token = getCookieValue("Authorization");
33 | if (token === null) {
34 | return;
35 | }
36 | const id = uuidv4();
37 | const res = await fetch("/api/v1/ask", {
38 | method: "POST",
39 | body: JSON.stringify({
40 | id: id,
41 | oslang: profile.user.language,
42 | tolang: lang,
43 | word: word,
44 | }),
45 | headers: {
46 | Authorization: token,
47 | },
48 | });
49 |
50 | if (!res.ok) {
51 | console.log("error");
52 | return;
53 | }
54 | setWordID(id);
55 | setRequested(true);
56 |
57 | const reader = res.body?.getReader();
58 |
59 | const decoder = new TextDecoder("utf-8");
60 |
61 | while (true) {
62 | if (reader === undefined) continue;
63 | const { done, value } = await reader.read();
64 | if (done) {
65 | console.log("breaked");
66 | break;
67 | }
68 | const text = decoder.decode(value);
69 | setMsgs((prev) => [...prev, text]);
70 | }
71 | setLoading(false);
72 | }
73 | return (
74 |
75 | {!requested && (
76 | <>
77 |
78 |
79 |
80 | {t.ASK.YOUR_LANG + " "}{" "}
81 |
82 | {Capitalize(lang)}
83 |
84 |
85 |
89 | {t.ASK.SELF}
90 |
91 |
92 |
setWord(e.target.value)}
96 | placeholder={t.ASK.INPUT_P}
97 | className="border border-zinc-300 rounded-2xl pl-6 py-3 focus:outline focus:outline-purple-500"
98 | />
99 |
100 |
101 | >
102 | )}
103 | {requested && (
104 |
105 |
106 | {msgs.map((txt) => {
107 | return txt;
108 | })}
109 |
110 |
111 | )}
112 |
113 | {requested ? (
114 |
118 | {t.ASK.B2}
119 |
120 | ) : (
121 |
126 | {t.ASK.B1}
127 |
128 | )}
129 |
130 |
131 | );
132 | }
133 |
134 | function Guide(props: { lang: string }) {
135 | const [open, setOpen] = useState(false);
136 | const { t } = useI8();
137 | return (
138 |
139 |
setOpen((prev) => !prev)}
141 | className="flex justify-between text-sm items-center cursor-pointer text-purple-800"
142 | >
143 | {t.ASK.G_M}
144 | {open ? : }
145 |
146 | {open && (
147 |
148 | {t.ASK.G1 + props.lang}
149 | {t.ASK.G2}
150 | {t.ASK.G3}
151 |
152 | )}
153 |
154 | );
155 | }
156 |
--------------------------------------------------------------------------------
/app/src/features/create_card/ui/main.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Navigate, useNavigate, useParams } from "react-router-dom";
3 | import { useCardStore } from "@/features/home";
4 | import { v4 as uuidv4 } from "uuid";
5 | import { CreateWord } from "@/api/word";
6 | import { cn } from "@/utils/cn";
7 | import { Copy } from "lucide-react";
8 | import { useAuthStore } from "@/features/common";
9 | import { useI8 } from "@/features/international";
10 |
11 | export function CreateWordPage() {
12 | const { lang } = useParams();
13 | const { t } = useI8();
14 | const navigate = useNavigate();
15 | const { profile } = useAuthStore();
16 | const [title, setTitle] = useState("");
17 | const [desc, setDesc] = useState("");
18 | const { addCard } = useCardStore();
19 | const [tab, setTab] = useState<"self" | "ai">("self");
20 |
21 | async function Save() {
22 | if (lang !== undefined) {
23 | const word = {
24 | id: uuidv4(),
25 | title: title,
26 | description: desc,
27 | created_at: "",
28 | updated_at: "",
29 | from_language: profile.user.language,
30 | to_language: lang,
31 | type: "self",
32 | };
33 | try {
34 | const data = await CreateWord(word);
35 | addCard(data);
36 | navigate("/app");
37 | } catch (error) {
38 | console.log(error);
39 | }
40 | }
41 | }
42 | if (
43 | lang === undefined ||
44 | !profile.languages.map((tl) => tl.name).includes(lang)
45 | ) {
46 | return ;
47 | }
48 | return (
49 |
50 |
51 | {t.CREATE.TITLE}
52 | setTitle(e.target.value)}
56 | placeholder={t.CREATE.TITLE_P}
57 | className="border border-zinc-300 rounded-2xl pl-6 py-3 focus:outline focus:outline-purple-500"
58 | />
59 |
60 |
61 |
{t.CREATE.DESC}
62 |
63 |
setTab("self")}
71 | >
72 | {t.CREATE.SELF}
73 |
74 |
setTab("ai")}
82 | >
83 | {t.CREATE.GEN}
84 |
85 |
86 | {tab === "self" ? (
87 |
98 |
99 |
103 | Сохранить
104 |
105 |
106 |
107 | );
108 | }
109 |
110 | function GenerateAI(props: { title: string; language: string }) {
111 | const { t } = useI8();
112 |
113 | function GetText() {
114 | switch (props.language) {
115 | case "english":
116 | return t.CREATE.P_EN;
117 |
118 | case "german":
119 | return t.CREATE.P_DE;
120 | default:
121 | return t.CREATE.P_EN;
122 | }
123 | }
124 |
125 | function GetPrompt() {
126 | let regex = /\[\[.*?\]\]/g;
127 | return GetText().replace(regex, props.title);
128 | }
129 | const copyToClipboard = () => {
130 | navigator.clipboard
131 | .writeText(GetPrompt())
132 | .then(() => {
133 | console.log("Text copied to clipboard");
134 | })
135 | .catch((err) => {
136 | console.error("Failed to copy text: ", err);
137 | });
138 | };
139 | return (
140 |
141 |
142 |
{t.CREATE.GEN1}
143 |
144 |
148 |
149 |
150 |
{GetPrompt()}
151 |
152 |
153 |
166 |
167 |
{t.CREATE.GEN3}
168 |
169 |
170 | );
171 | }
172 |
--------------------------------------------------------------------------------
/app/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as SelectPrimitive from "@radix-ui/react-select";
3 | import { Check, ChevronDown, ChevronUp } from "lucide-react";
4 |
5 | import { cn } from "@/utils/cn";
6 |
7 | const Select = SelectPrimitive.Root;
8 |
9 | const SelectGroup = SelectPrimitive.Group;
10 |
11 | const SelectValue = SelectPrimitive.Value;
12 |
13 | const SelectTrigger = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, children, ...props }, ref) => (
17 | span]:line-clamp-1",
21 | className
22 | )}
23 | {...props}
24 | >
25 | {children}
26 |
27 |
28 |
29 |
30 | ));
31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
32 |
33 | const SelectScrollUpButton = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 |
46 |
47 | ));
48 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
49 |
50 | const SelectScrollDownButton = React.forwardRef<
51 | React.ElementRef,
52 | React.ComponentPropsWithoutRef
53 | >(({ className, ...props }, ref) => (
54 |
62 |
63 |
64 | ));
65 | SelectScrollDownButton.displayName =
66 | SelectPrimitive.ScrollDownButton.displayName;
67 |
68 | const SelectContent = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, children, position = "popper", ...props }, ref) => (
72 |
73 |
84 |
85 |
92 | {children}
93 |
94 |
95 |
96 |
97 | ));
98 | SelectContent.displayName = SelectPrimitive.Content.displayName;
99 |
100 | const SelectLabel = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ));
110 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
111 |
112 | const SelectItem = React.forwardRef<
113 | React.ElementRef,
114 | React.ComponentPropsWithoutRef
115 | >(({ className, children, ...props }, ref) => (
116 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | {children}
131 |
132 | ));
133 | SelectItem.displayName = SelectPrimitive.Item.displayName;
134 |
135 | const SelectSeparator = React.forwardRef<
136 | React.ElementRef,
137 | React.ComponentPropsWithoutRef
138 | >(({ className, ...props }, ref) => (
139 |
144 | ));
145 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
146 |
147 | export {
148 | Select,
149 | SelectGroup,
150 | SelectValue,
151 | SelectTrigger,
152 | SelectContent,
153 | SelectLabel,
154 | SelectItem,
155 | SelectSeparator,
156 | SelectScrollUpButton,
157 | SelectScrollDownButton,
158 | };
159 |
--------------------------------------------------------------------------------
/app/src/features/common/ui/header.tsx:
--------------------------------------------------------------------------------
1 | import { Link, useLocation, useNavigate } from "react-router-dom";
2 | import {
3 | Play,
4 | Swords,
5 | CircleUserRound,
6 | FilePlus2,
7 | ChevronRight,
8 | } from "lucide-react";
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuLabel,
14 | DropdownMenuSeparator,
15 | DropdownMenuTrigger,
16 | } from "@/components/ui/dropdown-menu";
17 | import { cn } from "@/utils/cn";
18 | import { useAuthStore } from "..";
19 | import { useI8 } from "@/features/international";
20 | import { LanguageType } from "@/types/user";
21 | import { Capitalize } from "@/utils/string";
22 |
23 | export function Header() {
24 | const { profile, state } = useAuthStore();
25 | const { t } = useI8();
26 | const location = useLocation();
27 | const pathSegments = location.pathname
28 | .split("/")
29 | .filter((segment) => segment.length > 0);
30 | const firstSegment = (pathSegments.length > 0 ? pathSegments[1] : "") || "";
31 |
32 | return (
33 |
128 | );
129 | }
130 |
131 | function DirectToAsk(props: { language: string }) {
132 | const { t } = useI8();
133 | return (
134 |
138 |
139 | {t.HEADER.NEW}
140 |
141 | );
142 | }
143 |
144 | function MenuToAsk(props: { languages: LanguageType[] }) {
145 | const { t } = useI8();
146 | const navigate = useNavigate();
147 | return (
148 |
149 |
150 |
151 |
152 | {t.HEADER.NEW}
153 |
154 |
155 |
156 | {t.HEADER.CHOOSE}
157 |
158 | {props.languages.map((language) => {
159 | return (
160 | {
162 | navigate("/app/ask/" + language.name);
163 | }}
164 | key={language.id}
165 | className="py-2 flex items-center justify-between cursor-pointer"
166 | >
167 | {Capitalize(language.name)}
168 |
169 |
170 | );
171 | })}
172 |
173 |
174 | );
175 | }
176 |
--------------------------------------------------------------------------------
/app/src/features/onboarding/ui/main.tsx:
--------------------------------------------------------------------------------
1 | import { OnboardUpdate } from "@/api/me";
2 | import { cn } from "@/utils/cn";
3 | import { useEffect, useState } from "react";
4 | import { useNavigate } from "react-router-dom";
5 | import { queryClient } from "@/main";
6 |
7 | import { useI8 } from "@/features/international";
8 | import { OsLanguages, TargetLanguages } from "@/features/international";
9 |
10 | export function Onboarding() {
11 | const [step, setStep] = useState<"os" | "target">("os");
12 | const [osLang, setOsLang] = useState("");
13 | const { setLanguage } = useI8();
14 | const navigate = useNavigate();
15 | function OsStepHandle(value: string) {
16 | setOsLang(value);
17 | setLanguage(value);
18 | setStep("target");
19 | }
20 | async function TargetStepHandle(value: string[]) {
21 | if (osLang === "" || value.length === 0) {
22 | return;
23 | }
24 | try {
25 | await OnboardUpdate({
26 | os_language: osLang,
27 | target_languages: value,
28 | });
29 | queryClient.invalidateQueries("getme");
30 | navigate("/app?state=done");
31 | } catch (error) {
32 | console.log(error);
33 | }
34 | }
35 | return (
36 |
37 | {step === "os" && }
38 | {step === "target" && }
39 |
40 | );
41 | }
42 |
43 | export function OsLanguage(props: { Next: (value: string) => void }) {
44 | const [selected, setSelected] = useState("");
45 | const { setLanguage, t } = useI8();
46 | useEffect(() => {
47 | if (selected !== "") {
48 | setLanguage(selected);
49 | }
50 | }, [selected]);
51 | return (
52 | <>
53 |
54 |
55 | 1/2. {t.ONBOARD.THIS_H1}
56 |
57 |
58 | {t.ONBOARD.THIS_P1}
59 |
60 |
61 |
62 | {OsLanguages.map((lang) => {
63 | return (
64 |
setSelected(lang.value)}
74 | >
75 |
80 |
88 | {lang.text}
89 |
90 |
91 | );
92 | })}
93 |
94 |
95 | props.Next(selected)}
98 | className={cn("py-4 bg-purple-700 rounded-xl px-8 text-white", {
99 | "bg-zinc-400 cursor-not-allowed": selected === "",
100 | })}
101 | >
102 | {t.ONBOARD.THIS_B1}
103 |
104 |
105 | >
106 | );
107 | }
108 |
109 | export function TargetLanguage(props: {
110 | Complete: (selected: string[]) => void;
111 | }) {
112 | const [selected, setSelected] = useState([]);
113 | const { t } = useI8();
114 | return (
115 | <>
116 |
117 |
118 | 2/2. {t.ONBOARD.ANOTHER_H1}
119 |
120 |
121 | {t.ONBOARD.ANOTHER_P1}
122 | {selected.length === 0
123 | ? t.ONBOARD.ANOTHER_P2
124 | : t.ONBOARD.ANOTHER_P3 + selected.join(", ")}
125 |
126 |
127 |
128 | {TargetLanguages.map((lang) => {
129 | return (
130 |
{
141 | if (selected.includes(lang.value)) {
142 | setSelected((prev) =>
143 | prev.filter((value) => value !== lang.value)
144 | );
145 | } else {
146 | setSelected((prev) => [...prev, lang.value]);
147 | }
148 | }}
149 | >
150 |
155 |
163 | {lang.text}
164 |
165 |
166 | );
167 | })}
168 |
169 |
170 | props.Complete(selected)}
173 | className={cn("py-4 bg-purple-700 rounded-xl px-8 text-white", {
174 | "bg-zinc-400 cursor-not-allowed": selected.length === 0,
175 | })}
176 | >
177 | {t.ONBOARD.ANOTHER_B1}
178 |
179 |
180 | >
181 | );
182 | }
183 |
--------------------------------------------------------------------------------
/app/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3 | import { Check, ChevronRight, Circle } from "lucide-react"
4 |
5 | import { cn } from "@/utils/cn"
6 |
7 | const DropdownMenu = DropdownMenuPrimitive.Root
8 |
9 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
10 |
11 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
12 |
13 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
14 |
15 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
16 |
17 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
18 |
19 | const DropdownMenuSubTrigger = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef & {
22 | inset?: boolean
23 | }
24 | >(({ className, inset, children, ...props }, ref) => (
25 |
34 | {children}
35 |
36 |
37 | ))
38 | DropdownMenuSubTrigger.displayName =
39 | DropdownMenuPrimitive.SubTrigger.displayName
40 |
41 | const DropdownMenuSubContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, ...props }, ref) => (
45 |
53 | ))
54 | DropdownMenuSubContent.displayName =
55 | DropdownMenuPrimitive.SubContent.displayName
56 |
57 | const DropdownMenuContent = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, sideOffset = 4, ...props }, ref) => (
61 |
62 |
71 |
72 | ))
73 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
74 |
75 | const DropdownMenuItem = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef & {
78 | inset?: boolean
79 | }
80 | >(({ className, inset, ...props }, ref) => (
81 |
90 | ))
91 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
92 |
93 | const DropdownMenuCheckboxItem = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ className, children, checked, ...props }, ref) => (
97 |
106 |
107 |
108 |
109 |
110 |
111 | {children}
112 |
113 | ))
114 | DropdownMenuCheckboxItem.displayName =
115 | DropdownMenuPrimitive.CheckboxItem.displayName
116 |
117 | const DropdownMenuRadioItem = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, children, ...props }, ref) => (
121 |
129 |
130 |
131 |
132 |
133 |
134 | {children}
135 |
136 | ))
137 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
138 |
139 | const DropdownMenuLabel = React.forwardRef<
140 | React.ElementRef,
141 | React.ComponentPropsWithoutRef & {
142 | inset?: boolean
143 | }
144 | >(({ className, inset, ...props }, ref) => (
145 |
154 | ))
155 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
156 |
157 | const DropdownMenuSeparator = React.forwardRef<
158 | React.ElementRef,
159 | React.ComponentPropsWithoutRef
160 | >(({ className, ...props }, ref) => (
161 |
166 | ))
167 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
168 |
169 | const DropdownMenuShortcut = ({
170 | className,
171 | ...props
172 | }: React.HTMLAttributes) => {
173 | return (
174 |
178 | )
179 | }
180 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
181 |
182 | export {
183 | DropdownMenu,
184 | DropdownMenuTrigger,
185 | DropdownMenuContent,
186 | DropdownMenuItem,
187 | DropdownMenuCheckboxItem,
188 | DropdownMenuRadioItem,
189 | DropdownMenuLabel,
190 | DropdownMenuSeparator,
191 | DropdownMenuShortcut,
192 | DropdownMenuGroup,
193 | DropdownMenuPortal,
194 | DropdownMenuSub,
195 | DropdownMenuSubContent,
196 | DropdownMenuSubTrigger,
197 | DropdownMenuRadioGroup,
198 | }
199 |
--------------------------------------------------------------------------------