├── 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 | 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 | 14 | SW 15 |
16 | 17 | 21 | {button} 22 | 23 |
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 | 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 | 24 | 25 | {step}{" "} 26 | / {card?.words.length} 27 | 28 | {card !== null && step < card?.words.length ? ( 29 | 36 | ) : ( 37 | 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 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) 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 | {profile.user.name} 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 | 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 |
14 |
15 |

16 | {t.LOGIN.WELCOME} 17 |

18 | 19 | {t.LOGIN.NEXT} 20 | 21 |
22 | 26 | 27 | {t.LOGIN.GOOGLE} 28 | 29 |
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 |