diff --git a/.github/images/favicon.ico b/.github/images/favicon.ico new file mode 100644 index 0000000..f8810a4 Binary files /dev/null and b/.github/images/favicon.ico differ diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml new file mode 100644 index 0000000..f3d64ba --- /dev/null +++ b/.github/workflows/deployment.yaml @@ -0,0 +1,41 @@ +name: deployment + +on: + workflow_dispatch: + push: + branches: + - gh-pages + paths-ignore: + - 'dist/**' + - 'docs/**' + +jobs: + build: + env: + CI_COMMIT_MESSAGE: updates CI/CD Build Artifacts + CI_COMMIT_AUTHOR: Continuous Integration + runs-on: ubuntu-latest + + steps: + - name: 🌿 Check out branch + uses: actions/checkout@v3 + + - name: 💽 Install dependencies + run: | + npm ci + + - name: 🚧 Build project for production + run: | + rm -r ./docs + npm run build-prod + cp -r "./docs/browser/" "./docs" + rm -r ./docs/browser + cp "./docs/index.html" "./docs/404.html" + + - name: 🎉 Commit & Push + run: | + git add . + git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" + git config --global user.email "username@users.noreply.github.com" + git commit -a -m "${{ env.CI_COMMIT_MESSAGE }}" + git push diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7d3de83 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +MIT License + +Copyright 2024 Stephanie Hohenberg + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 8304639..299d352 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,35 @@ -# Caligraphy +

+ + Chinese Puzzle +

-This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.4. +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?color=blue)](./LICENSE) -## Development server +### Introduction +Chinese Puzzle is an innovative platform designed for learning the Chinese language and exploring its complex characters. +The core components include: +- Radicals Overview: A comprehensive guide to the root elements of Chinese characters, known as radicals. +- Character Visualization: Engaging visuals that illustrate how characters are formed from these root elements. -Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. +Through the deconstruction of characters into root elements — a process known as chanzi — users can enhance their ability to recognize and memorize characters, significantly improving their learning experience. +Additionally, users can expand their vocabulary by browsing specific topic clusters or exploring radicals and their occurrences in various Chinese characters. -## Code scaffolding -Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. +Features (WIP) +- Internationalization: The platform is available in 🇺🇸 English and 🇩🇪 German. +- Vocabulary: Access a collection of the 1,000 most common Chinese words. +- Chanzi Analysis: Interactively explore Chinese characters and their compositions. +- Search Functionality: Search for characters to discover their chanzi and occurrences in other characters. +- Chanzi via Machine Learning: For characters not included in the platform's vocabulary, machine learning is utilized to deconstruct them into root elements. -## Build +Explore the Chinese Puzzle and embark on your journey to mastering the Chinese language! -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. -## Running unit tests +### Local Setup -Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). +The tool is deployed as a Github Page. -## Running end-to-end tests +But in case you would like to run the project locally, follow these steps: -Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. - -## Further help - -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. +- Install dependencies: `npm install` +- Start project: `npm run start` diff --git a/angular.json b/angular.json index e4cc881..f8ea029 100644 --- a/angular.json +++ b/angular.json @@ -13,7 +13,7 @@ "build": { "builder": "@angular-devkit/build-angular:application", "options": { - "outputPath": "dist/caligraphy", + "outputPath": "docs", "index": "src/index.html", "browser": "src/main.ts", "polyfills": [ @@ -21,12 +21,11 @@ ], "tsConfig": "tsconfig.app.json", "assets": [ - { - "glob": "**/*", - "input": "public" - } + "src/favicon.ico", + "src/assets" ], "styles": [ + "@angular/material/prebuilt-themes/magenta-violet.css", "src/styles.css" ], "scripts": [] @@ -36,8 +35,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kB", - "maximumError": "1MB" + "maximumWarning": "1MB", + "maximumError": "2MB" }, { "type": "anyComponentStyle", @@ -79,12 +78,11 @@ ], "tsConfig": "tsconfig.spec.json", "assets": [ - { - "glob": "**/*", - "input": "public" - } + "src/favicon.ico", + "src/assets" ], "styles": [ + "@angular/material/prebuilt-themes/magenta-violet.css", "src/styles.css" ], "scripts": [] diff --git a/docs/gh-pages.md b/docs/gh-pages.md new file mode 100644 index 0000000..a55547b --- /dev/null +++ b/docs/gh-pages.md @@ -0,0 +1,6 @@ +## Info + +This folder is used for the files of the github page in the branch gh-pages +https://stephaniehhnbrg.github.io/chinese-caligraphy/ + +Check out the Github Action or the Github Workflow deployment.yaml for more insights. diff --git a/package-lock.json b/package-lock.json index e00fd7c..d582d20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,19 @@ "version": "0.0.0", "dependencies": { "@angular/animations": "^18.2.0", + "@angular/cdk": "^18.2.4", "@angular/common": "^18.2.0", "@angular/compiler": "^18.2.0", "@angular/core": "^18.2.0", "@angular/forms": "^18.2.0", + "@angular/material": "^18.2.4", "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", + "ngx-translate": "^0.0.1-security", + "ngx-translate-in": "^0.0.2", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" @@ -342,6 +348,22 @@ } } }, + "node_modules/@angular/cdk": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.4.tgz", + "integrity": "sha512-o+TuxZDqStfkviEkCR05pVyP6R2RIruEs/45Cms76hlsIheMoxRaxir/yrHdh4tZESJJhcO/EVE+aymNIRWAfg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^18.0.0 || ^19.0.0", + "@angular/core": "^18.0.0 || ^19.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/cli": { "version": "18.2.4", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.4.tgz", @@ -469,6 +491,23 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/material": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.4.tgz", + "integrity": "sha512-F09145mI/EAHY9ngdnQTo3pFRmUoU/50i6cmddtL4cse0WidatoodQr0gZCksxhmpJgRy5mTcjh/LU2hShOgcA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^18.0.0 || ^19.0.0", + "@angular/cdk": "18.2.4", + "@angular/common": "^18.0.0 || ^19.0.0", + "@angular/core": "^18.0.0 || ^19.0.0", + "@angular/forms": "^18.0.0 || ^19.0.0", + "@angular/platform-browser": "^18.0.0 || ^19.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/platform-browser": { "version": "18.2.4", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.4.tgz", @@ -3319,6 +3358,33 @@ "webpack": "^5.54.0" } }, + "node_modules/@ngx-translate/core": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-15.0.0.tgz", + "integrity": "sha512-Am5uiuR0bOOxyoercDnAA3rJVizo4RRqJHo8N3RqJ+XfzVP/I845yEnMADykOHvM6HkVm4SZSnJBOiz0Anx5BA==", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-8.0.0.tgz", + "integrity": "sha512-SFMsdUcmHF5OdZkL1CHEoSAwbP5EbAOPTLLboOCRRoOg21P4GJx+51jxGdJeGve6LSKLf4Pay7BkTwmE6vxYlg==", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@ngx-translate/core": ">=15.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6137,7 +6203,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.12" }, @@ -9255,6 +9321,31 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/ngx-translate": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/ngx-translate/-/ngx-translate-0.0.1-security.tgz", + "integrity": "sha512-IkSARdHmTjKVtD0FsmbztF/LORxPcr3Bfv0SutDIMVGGLXwb8H3Y7062fA+zW0gOS56nxiTJLtxEd63DCTMfIQ==" + }, + "node_modules/ngx-translate-in": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/ngx-translate-in/-/ngx-translate-in-0.0.2.tgz", + "integrity": "sha512-IIwPp7sB3ZVaJVy7Sw0BjJ9uUCM2jvF92cPe1qkoL854X43Do1cwSaN1Te0qkwH4ypCnFnsB3Ic5Etv7SUjoQg==", + "dependencies": { + "tslib": "^1.10.0" + }, + "peerDependencies": { + "@angular/common": "^9.1.12", + "@angular/core": "^9.1.12", + "@ngx-translate/core": "^11.0.0", + "@ngx-translate/http-loader": "^4.0.0", + "rxjs": "^6.0.0" + } + }, + "node_modules/ngx-translate-in/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", @@ -9982,7 +10073,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, + "devOptional": true, "dependencies": { "entities": "^4.4.0" }, diff --git a/package.json b/package.json index a5af58e..925d925 100644 --- a/package.json +++ b/package.json @@ -5,19 +5,25 @@ "ng": "ng", "start": "ng serve", "build": "ng build", + "build-prod": "ng build --configuration production --base-href https://stephaniehhnbrg.github.io/chinese-caligraphy/", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "^18.2.0", + "@angular/cdk": "^18.2.4", "@angular/common": "^18.2.0", "@angular/compiler": "^18.2.0", "@angular/core": "^18.2.0", "@angular/forms": "^18.2.0", + "@angular/material": "^18.2.4", "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", + "ngx-translate": "^0.0.1-security", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 57614f9..0000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/src/app/app.component.html b/src/app/app.component.html index 36093e1..21f8668 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,336 +1,2 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - - + + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 9406604..d742011 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,13 +1,16 @@ import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import {TranslateService} from "@ngx-translate/core"; +import {CharacterService} from "./services/character.service"; @Component({ selector: 'app-root', - standalone: true, - imports: [RouterOutlet], templateUrl: './app.component.html', styleUrl: './app.component.css' }) export class AppComponent { - title = 'caligraphy'; + + constructor(private translate: TranslateService) { + this.translate.setDefaultLang('en'); + this.translate.use('en'); + } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts deleted file mode 100644 index a1e7d6f..0000000 --- a/src/app/app.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; -import { provideRouter } from '@angular/router'; - -import { routes } from './app.routes'; - -export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)] -}; diff --git a/src/app/app.module.ts b/src/app/app.module.ts new file mode 100644 index 0000000..223e042 --- /dev/null +++ b/src/app/app.module.ts @@ -0,0 +1,91 @@ +import {NgModule, provideZoneChangeDetection} from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { HttpClientModule, HttpClient } from '@angular/common/http'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; + +import { AppComponent } from './app.component'; +import {routes} from "./app.routes"; +import { provideRouter, RouterOutlet } from '@angular/router'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import {CommonModule} from "@angular/common"; +import {ToolbarComponent} from "./components/toolbar/toolbar.component"; +import {MatToolbarModule} from "@angular/material/toolbar"; +import {MatButtonModule} from "@angular/material/button"; +import {MatIconModule} from "@angular/material/icon"; +import {MatMenuModule} from "@angular/material/menu"; +import {RadicalOverviewComponent} from "./components/radical-overview/radical-overview.component"; +import {MatSlideToggleModule} from "@angular/material/slide-toggle"; +import {ColorSlideToggleComponent} from "./components/color-slide-toggle/color-slide-toggle.component"; +import { + PinyinToneIntroductionComponent +} from "./components/pinyin-tone-introduction/pinyin-tone-introduction.component"; +import {HomePageComponent} from "./pages/home-page/home-page.component"; +import { + CharactersByRadicalsSidecarComponent +} from "./components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component"; +import {MatSidenavModule} from "@angular/material/sidenav"; +import {AggregatedPageComponent} from "./pages/aggregated-page/aggregated-page.component"; +import {MatTooltipModule} from "@angular/material/tooltip"; +import {CalendarPageComponent} from "./pages/calendar-page/calendar-page.component"; +import {TopicBannerComponent} from "./components/topic-banner/topic-banner.component"; +import {CharacterService} from "./services/character.service"; +import {MatButtonToggleModule} from "@angular/material/button-toggle"; +import {RadicalGridComponent} from "./components/radical-grid/radical-grid.component"; +import {RadicalCarouselComponent} from "./components/radical-carousel/radical-carousel.component"; +import {MatCardModule} from "@angular/material/card"; + +export function HttpLoaderFactory(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/i18n/', '.json'); +} + +@NgModule({ + declarations: [ + AppComponent, + ToolbarComponent, + AggregatedPageComponent, + HomePageComponent, + PinyinToneIntroductionComponent, + RadicalOverviewComponent, + RadicalGridComponent, + RadicalCarouselComponent, + CharactersByRadicalsSidecarComponent, + ColorSlideToggleComponent, + CalendarPageComponent, + TopicBannerComponent, + ], + imports: [ + BrowserModule, + BrowserAnimationsModule, + CommonModule, + RouterOutlet, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }), + + // Angular Material Modules + MatToolbarModule, + MatButtonModule, + MatButtonToggleModule, + MatIconModule, + MatMenuModule, + MatSlideToggleModule, + MatSidenavModule, + MatTooltipModule, + MatCardModule, + ], + providers: [ + provideRouter(routes), + provideZoneChangeDetection({ eventCoalescing: true }), + provideAnimationsAsync(), + CharacterService, + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb..1521adc 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,8 @@ import { Routes } from '@angular/router'; +import {AggregatedPageComponent} from "./pages/aggregated-page/aggregated-page.component"; +import {CalendarPageComponent} from "./pages/calendar-page/calendar-page.component"; -export const routes: Routes = []; +export const routes: Routes = [ + {path:'calendar', component: CalendarPageComponent}, + {path:'', component: AggregatedPageComponent} +]; diff --git a/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.css b/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.css new file mode 100644 index 0000000..2bdeb56 --- /dev/null +++ b/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.css @@ -0,0 +1,84 @@ +.sidecar { + padding: 5px; + border-right: dimgrey 1px solid; + height: 99%; + overflow: hidden; +} + +.close-icon { + float: right; + font-size: smaller; +} + +.close-icon:hover { + cursor: pointer; + color: dimgrey; +} + +.word { + padding-bottom: 15px; +} + +.character { + display: inline-block; + flex-direction: column; +} + +.hanzi { + border-radius: 10px; + border: 1px solid dimgrey; + width: auto; + padding: 3px; + text-align: center; + margin-right: 10px; +} + +.emoji { + display: none; +} + +.hanzi:hover .emoji { + display: block; +} + +.pinyin { + font-size: xx-small; + text-align: center; + padding: 3px; + white-space: nowrap; +} + +.composition { + font-size: xx-small; +} + +.compositions { + flex-wrap: wrap; +} + +.radical { + margin: 3px; + padding: 3px; + height: 20px; + border-radius: 10px; + border: 1px solid white; +} + +.radical:hover{ + background-color: white; + color: black; + border: 1px solid dimgrey; +} + +.highlighted { + background-color: blue; + color: white; + border: 0; + cursor: pointer; +} + +.plus { + margin: 3px; + padding: 3px; + height: 20px; +} diff --git a/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.html b/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.html new file mode 100644 index 0000000..b0f701b --- /dev/null +++ b/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.html @@ -0,0 +1,36 @@ +
+ close +

{{"SIDECAR.TITLE" | translate}}

+

+ +
+
+
+
+ {{c.emoji}} + {{c.hanzi}} +
+ {{c.pinyin}} ({{ c.translationKey | translate }}) +
+
+
+ +
+
+
+ {{sign}} +
+ + +
+
+ +
+ + +
+
diff --git a/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.spec.ts b/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.spec.ts new file mode 100644 index 0000000..07fcc6e --- /dev/null +++ b/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CharactersByRadicalsSidecarComponent } from './characters-by-radicals-sidecar.component'; + +describe('CharactersByRadicalsSidecarComponent', () => { + let component: CharactersByRadicalsSidecarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CharactersByRadicalsSidecarComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CharactersByRadicalsSidecarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.ts b/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.ts new file mode 100644 index 0000000..6df511f --- /dev/null +++ b/src/app/components/characters-by-radicals-sidecar/characters-by-radicals-sidecar.component.ts @@ -0,0 +1,114 @@ +import {Component, EventEmitter, OnDestroy, OnInit, Output} from '@angular/core'; +import {ActivatedRoute, Router} from "@angular/router"; +import {Subscription} from "rxjs"; +import {Word} from "../../interfaces/word.data"; +import {CharacterService} from "../../services/character.service"; +import {TranslateService} from "@ngx-translate/core"; + + +@Component({ + selector: 'app-characters-by-radicals-sidecar', + templateUrl: './characters-by-radicals-sidecar.component.html', + styleUrl: './characters-by-radicals-sidecar.component.css' +}) +export class CharactersByRadicalsSidecarComponent implements OnInit, OnDestroy { + + public characters: Word[] = []; + public radicals = ""; + + private subscriptions: Subscription[] = []; + @Output() closeIconClicked = new EventEmitter(); + + constructor(private route: ActivatedRoute, + private router: Router, + private characterService: CharacterService, + private translate: TranslateService) {} + ngOnInit() { + this.subscriptions.push(this.route.queryParams + .subscribe(params => { + if (params['radicals']) { + this.radicals = ""+params['radicals'].replaceAll('&', ', '); + this.characters = []; + this.radicals.split(', ').forEach(r => { + // TODO && statt || for multiple radicals + this.characters = this.characters.concat(this.characterService.getOccurencesOfSign(r)); + }) + } + })); + } + + public isSelectedRadical(r: string): boolean { + return this.radicals.includes(r); + } + + public getComposition(comp: string[]): string[] { + let result: string[] = []; + comp.forEach(c => { + if (this.characterService.isRadical(c)) { + result.push(c); + } else { + result = result.concat(this.characterService.getComposition(c)); + console.log(c + " result"+ result); + } + }); + return result; + } + + public getTooltip(sign: string): string { + if (this.characterService.isRadical(sign)) { + let radical = this.characterService.getRadical(sign)!; + let tooltip = `#${radical.index} ` + (radical.emoji || '') + " "; + if (radical.pinyin.length > 0) { + tooltip += radical.pinyin + ' \n' + } + if (radical.translationKey.length > 0) { + tooltip += this.translate.instant(radical.translationKey) + ' \n'; + } + if (radical.tooltip) { + let parts = radical.tooltip.split(':n='); + let translateKey = parts[0]; + let param = parts.length == 2 ? {n: +parts[1]}: undefined; + tooltip += this.translate.instant(translateKey, param); + } + return tooltip; + } else { + let word = this.characterService.getWord(sign); + if (word) { + let tooltip = (word.emoji || '') + " "; + if (word.pinyin.length > 0) { + tooltip += word.pinyin + ' \n' + } + if (word.translationKey.length > 0) { + tooltip += this.translate.instant(word.translationKey) + ' \n' + } + tooltip += word.composition[0].reduce((prev, curr) => prev + " " + curr); + return tooltip; + } + + } + + return ""; + } + public closeDrawer() { + this.closeIconClicked.emit(true); + this.router.navigate( + ['/'], + { queryParams: { view: this.route.snapshot.queryParams['view'] }} + ); + } + public ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + public scrollToRadical(r: string) { + let isRadical = this.characterService.isRadical(r); + if (isRadical) { + this.router.navigate( + ['/'], + { queryParams: { radicals: r, view: this.route.snapshot.queryParams['view'] }} + ); + } else { + // TODO: WordView + } + } +} diff --git a/src/app/components/color-slide-toggle/color-slide-toggle.component.css b/src/app/components/color-slide-toggle/color-slide-toggle.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/color-slide-toggle/color-slide-toggle.component.html b/src/app/components/color-slide-toggle/color-slide-toggle.component.html new file mode 100644 index 0000000..5ed77d6 --- /dev/null +++ b/src/app/components/color-slide-toggle/color-slide-toggle.component.html @@ -0,0 +1,2 @@ + + diff --git a/src/app/components/color-slide-toggle/color-slide-toggle.component.spec.ts b/src/app/components/color-slide-toggle/color-slide-toggle.component.spec.ts new file mode 100644 index 0000000..71dc245 --- /dev/null +++ b/src/app/components/color-slide-toggle/color-slide-toggle.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ColorSlideToggleComponent } from './color-slide-toggle.component'; + +describe('ColorSlideToggleComponent', () => { + let component: ColorSlideToggleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ColorSlideToggleComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ColorSlideToggleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/color-slide-toggle/color-slide-toggle.component.ts b/src/app/components/color-slide-toggle/color-slide-toggle.component.ts new file mode 100644 index 0000000..d4d74cd --- /dev/null +++ b/src/app/components/color-slide-toggle/color-slide-toggle.component.ts @@ -0,0 +1,36 @@ +import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core'; + +@Component({ + selector: 'app-color-slide-toggle', + templateUrl: './color-slide-toggle.component.html', + styleUrl: './color-slide-toggle.component.css' +}) +export class ColorSlideToggleComponent implements AfterViewInit { + + @ViewChild('darkModeSwitch', { read: ElementRef }) element: ElementRef | undefined; + + sun = 'M12 15.5q1.45 0 2.475-1.025Q15.5 13.45 15.5 12q0-1.45-1.025-2.475Q13.45 8.5 12 8.5q-1.45 0-2.475 1.025Q8.5 10.55 8.5 12q0 1.45 1.025 2.475Q10.55 15.5 12 15.5Zm0 1.5q-2.075 0-3.537-1.463T7 12q0-2.075 1.463-3.537T12 7q2.075 0 3.537 1.463T17 12q0 2.075-1.463 3.537T12 17ZM1.75 12.75q-.325 0-.538-.213Q1 12.325 1 12q0-.325.212-.537Q1.425 11.25 1.75 11.25h2.5q.325 0 .537.213Q5 11.675 5 12q0 .325-.213.537-.213.213-.537.213Zm18 0q-.325 0-.538-.213Q19 12.325 19 12q0-.325.212-.537.212-.213.538-.213h2.5q.325 0 .538.213Q23 11.675 23 12q0 .325-.212.537-.212.213-.538.213ZM12 5q-.325 0-.537-.213Q11.25 4.575 11.25 4.25v-2.5q0-.325.213-.538Q11.675 1 12 1q.325 0 .537.212 .213.212 .213.538v2.5q0 .325-.213.537Q12.325 5 12 5Zm0 18q-.325 0-.537-.212-.213-.212-.213-.538v-2.5q0-.325.213-.538Q11.675 19 12 19q.325 0 .537.212 .213.212 .213.538v2.5q0 .325-.213.538Q12.325 23 12 23ZM6 7.05l-1.425-1.4q-.225-.225-.213-.537.013-.312.213-.537.225-.225.537-.225t.537.225L7.05 6q.2.225 .2.525 0 .3-.2.5-.2.225-.513.225-.312 0-.537-.2Zm12.35 12.375L16.95 18q-.2-.225-.2-.538t.225-.512q.2-.225.5-.225t.525.225l1.425 1.4q.225.225 .212.538-.012.313-.212.538-.225.225-.538.225t-.538-.225ZM16.95 7.05q-.225-.225-.225-.525 0-.3.225-.525l1.4-1.425q.225-.225.538-.213.313 .013.538 .213.225 .225.225 .537t-.225.537L18 7.05q-.2.2-.512.2-.312 0-.538-.2ZM4.575 19.425q-.225-.225-.225-.538t.225-.538L6 16.95q.225-.225.525-.225.3 0 .525.225 .225.225 .225.525 0 .3-.225.525l-1.4 1.425q-.225.225-.537.212-.312-.012-.537-.212ZM12 12Z' + moon ='M12 21q-3.75 0-6.375-2.625T3 12q0-3.75 2.625-6.375T12 3q.2 0 .425.013 .225.013 .575.038-.9.8-1.4 1.975-.5 1.175-.5 2.475 0 2.25 1.575 3.825Q14.25 12.9 16.5 12.9q1.3 0 2.475-.463T20.95 11.15q.025.3 .038.488Q21 11.825 21 12q0 3.75-2.625 6.375T12 21Zm0-1.5q2.725 0 4.75-1.687t2.525-3.963q-.625.275-1.337.412Q17.225 14.4 16.5 14.4q-2.875 0-4.887-2.013T9.6 7.5q0-.6.125-1.287.125-.687.45-1.562-2.45.675-4.062 2.738Q4.5 9.45 4.5 12q0 3.125 2.188 5.313T12 19.5Zm-.1-7.425Z' + + constructor() { + } + + public changeColor(lightMode: boolean) { + let body = document.querySelector('body'); + if (body) { + if (lightMode) { + body.classList.add('lightMode'); + body.classList.remove('darkMode'); + } else { + body.classList.add('darkMode'); + body.classList.remove('lightMode'); + } + } + } + ngAfterViewInit() { + if (this.element){ + this.element.nativeElement.querySelector('.mdc-switch__icon--on').firstChild.setAttribute('d', this.sun); + this.element.nativeElement.querySelector('.mdc-switch__icon--off').firstChild.setAttribute('d', this.moon); + } + } +} diff --git a/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.css b/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.css @@ -0,0 +1 @@ + diff --git a/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.html b/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.html new file mode 100644 index 0000000..5d191d8 --- /dev/null +++ b/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.html @@ -0,0 +1,36 @@ + +
+

{{"PINYIN.TITLE" | translate}}

+

{{"PINYIN.PINYIN_INTRODUCTION" | translate}}

+ +

{{"PINYIN.TONE_INTRODUCTION" | translate}}

+
    +
  • {{"PINYIN.TONE_1" | translate}}
  • +
  • {{"PINYIN.TONE_2" | translate}}
  • +
  • {{"PINYIN.TONE_3" | translate}}
  • +
  • {{"PINYIN.TONE_4" | translate}}
  • +
+ +

 

+ +

{{"PINYIN.DOUBLE_VOCALS.EXPLANATION" | translate}}

+
    +
  1. {{"PINYIN.DOUBLE_VOCALS.RULE_1" | translate}}
  2. +
  3. {{"PINYIN.DOUBLE_VOCALS.RULE_2" | translate}}
  4. +
  5. {{"PINYIN.DOUBLE_VOCALS.RULE_3" | translate}}
  6. +
+ +

 

+ +

{{"PINYIN.CHANGING_INTONATION.EXPLANATION" | translate}}

+
    +
  • {{"PINYIN.CHANGING_INTONATION.2_TONES" | translate}}
  • +
  • {{"PINYIN.CHANGING_INTONATION.3_TONES" | translate}}
  • +
  • {{"PINYIN.CHANGING_INTONATION.4_TONES" | translate}}
  • +
+

{{"PINYIN.CHANGING_INTONATION.BU" | translate}}

+
    +
  • {{"PINYIN.CHANGING_INTONATION.BU_EXAMPLE_1" | translate}}
  • +
  • {{"PINYIN.CHANGING_INTONATION.BU_EXAMPLE_2" | translate}}
  • +
+
diff --git a/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.spec.ts b/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.spec.ts new file mode 100644 index 0000000..295c4d8 --- /dev/null +++ b/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PinyinToneIntroductionComponent } from './pinyin-tone-introduction.component'; + +describe('PinyinToneIntroductionComponent', () => { + let component: PinyinToneIntroductionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PinyinToneIntroductionComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PinyinToneIntroductionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.ts b/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.ts new file mode 100644 index 0000000..df3a23c --- /dev/null +++ b/src/app/components/pinyin-tone-introduction/pinyin-tone-introduction.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-pinyin-tone-introduction', + templateUrl: './pinyin-tone-introduction.component.html', + styleUrl: './pinyin-tone-introduction.component.css' +}) +export class PinyinToneIntroductionComponent { + +} diff --git a/src/app/components/radical-carousel/radical-carousel.component.css b/src/app/components/radical-carousel/radical-carousel.component.css new file mode 100644 index 0000000..3e69973 --- /dev/null +++ b/src/app/components/radical-carousel/radical-carousel.component.css @@ -0,0 +1,68 @@ +.carousel { + align-items: center; + justify-content: center; +} + +.frame { + width: 70%; +} + +.grey-box-parent { + margin-right: 20px; + margin-bottom: 20px; +} + +.grey-box { + background-color: blue; + color: white; + height: 100px; + width: 150px; + border-radius: 5px; + padding: 5px; + text-align: center; + display:table-cell; + vertical-align: middle; + font-size: xx-large; +} + +.emoji { + display: none; +} + +.grey-box:hover .emoji { + display: block; +} + + +.occurences { + flex-wrap: wrap; + height: 70px; + overflow-y: scroll; +} + +.character { + display: inline-block; + flex-direction: column; + margin-bottom: 10px; +} + +.character:hover { + font-size: x-large; + cursor: zoom-in; +} + +.hanzi { + border-radius: 10px; + border: 1px solid dimgrey; + width: auto; + padding: 5px 3px 3px; + text-align: center; + margin-right: 10px; +} + +.pinyin { + font-size: xx-small; + text-align: center; + padding: 3px; + white-space: nowrap; +} diff --git a/src/app/components/radical-carousel/radical-carousel.component.html b/src/app/components/radical-carousel/radical-carousel.component.html new file mode 100644 index 0000000..36657c1 --- /dev/null +++ b/src/app/components/radical-carousel/radical-carousel.component.html @@ -0,0 +1,48 @@ + diff --git a/src/app/components/radical-carousel/radical-carousel.component.spec.ts b/src/app/components/radical-carousel/radical-carousel.component.spec.ts new file mode 100644 index 0000000..60bd85c --- /dev/null +++ b/src/app/components/radical-carousel/radical-carousel.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RadicalCarouselComponent } from './radical-carousel.component'; + +describe('RadicalCarouselComponent', () => { + let component: RadicalCarouselComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RadicalCarouselComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RadicalCarouselComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/radical-carousel/radical-carousel.component.ts b/src/app/components/radical-carousel/radical-carousel.component.ts new file mode 100644 index 0000000..dd62ac7 --- /dev/null +++ b/src/app/components/radical-carousel/radical-carousel.component.ts @@ -0,0 +1,58 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Radical} from "../../interfaces/radical.data"; +import {RADICALS} from "../../data/radicals"; +import {TranslateService} from "@ngx-translate/core"; +import {ActivatedRoute, Router} from "@angular/router"; +import {CharacterService} from "../../services/character.service"; +import {Subscription} from "rxjs"; +import {Word} from "../../interfaces/word.data"; + +@Component({ + selector: 'app-radical-carousel', + templateUrl: './radical-carousel.component.html', + styleUrl: './radical-carousel.component.css' +}) +export class RadicalCarouselComponent implements OnInit, OnDestroy { + + public radicals: Radical[] = RADICALS; + public displayedRadicalIndex: number = 0; + private subscriptions: Subscription[] = []; + + constructor(private translate: TranslateService, + private route: ActivatedRoute, + private router: Router, + private characterService: CharacterService) {} + + public ngOnInit() { + this.subscriptions.push(this.route.queryParams + .subscribe(params => { + let routeIndex = this.radicals.findIndex(r => params['radicals'].includes(r.sign)); + this.displayedRadicalIndex = routeIndex > -1 ? routeIndex : 0; + })); + } + prev() { + this.displayedRadicalIndex = this.displayedRadicalIndex > 0 ? this.displayedRadicalIndex - 1 : this.radicals.length -1; + this.router.navigate(['/'], { queryParams: { radicals: this.radicals[this.displayedRadicalIndex].sign, view: 'single' }}); + } + + next() { + this.displayedRadicalIndex = this.displayedRadicalIndex < (this.radicals.length -1) ? this.displayedRadicalIndex + 1 : 0; + this.router.navigate(['/'], { queryParams: { radicals: this.radicals[this.displayedRadicalIndex].sign, view: 'single' }}); + } + + getInstantedTooltip(tooltip: string): string { + let parts = tooltip.split(':n='); + let translateKey = parts[0]; + let param = parts.length == 2 ? {n: +parts[1]}: undefined; + return this.translate.instant(translateKey, param); + } + + getRadicalsOccurences(radical: Radical): Word[] { + console.log(this.characterService.getOccurencesOfSign(radical.sign)); + return this.characterService.getOccurencesOfSign(radical.sign); + } + + public ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } +} diff --git a/src/app/components/radical-grid/radical-grid.component.css b/src/app/components/radical-grid/radical-grid.component.css new file mode 100644 index 0000000..289524c --- /dev/null +++ b/src/app/components/radical-grid/radical-grid.component.css @@ -0,0 +1,55 @@ +.grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.radical-container, .tile-button { + margin: 10px; + width: 150px; +} + +.grey-box { + background-color: dimgrey; + color: white; + height: 100px; + width: 150px; + border-radius: 5px; + padding: 5px; + text-align: center; + display:table-cell; + vertical-align: middle; + font-size: xx-large; +} + +.radical-container > .grey-box:hover { + background-color: darkorchid; + font-size: xxx-large; + cursor: pointer; +} + +.emoji { + display: none; +} + +::ng-deep .radical-tooltip { + white-space: pre-line; + text-align: left !important; +} + +.radical-container > .grey-box:hover .emoji { + display: block; +} + +.tile-button { + font-size: medium; +} + +.tile-button:hover { + background-color: darkslategray; + cursor: pointer; +} + +.selected-radical { + background-color: blue; +} diff --git a/src/app/components/radical-grid/radical-grid.component.html b/src/app/components/radical-grid/radical-grid.component.html new file mode 100644 index 0000000..679189c --- /dev/null +++ b/src/app/components/radical-grid/radical-grid.component.html @@ -0,0 +1,23 @@ +
+
+
+ {{r.emoji}} + {{r.sign}} +
+ + #00{{r.index}} {{r.pinyin}}
+ {{r.translationKey}} +
+
+
+ + {{"RADICAL_OVERVIEW.SHOW_MORE" | translate}} +
+
+ - {{"RADICAL_OVERVIEW.SHOW_LESS" | translate}} +
+
diff --git a/src/app/components/radical-grid/radical-grid.component.spec.ts b/src/app/components/radical-grid/radical-grid.component.spec.ts new file mode 100644 index 0000000..34a0025 --- /dev/null +++ b/src/app/components/radical-grid/radical-grid.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RadicalGridComponent } from './radical-grid.component'; + +describe('RadicalGridComponent', () => { + let component: RadicalGridComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RadicalGridComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RadicalGridComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/radical-grid/radical-grid.component.ts b/src/app/components/radical-grid/radical-grid.component.ts new file mode 100644 index 0000000..8bd003a --- /dev/null +++ b/src/app/components/radical-grid/radical-grid.component.ts @@ -0,0 +1,138 @@ +import {AfterViewInit, Component, EventEmitter, inject, OnDestroy, OnInit, Output} from '@angular/core'; +import {Radical} from "../../interfaces/radical.data"; +import {TranslateService} from "@ngx-translate/core"; +import {ActivatedRoute, Router} from "@angular/router"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {Subscription} from "rxjs"; +import {RADICALS} from "../../data/radicals"; +import {CharacterService} from "../../services/character.service"; + +@Component({ + selector: 'app-radical-grid', + templateUrl: './radical-grid.component.html', + styleUrl: './radical-grid.component.css' +}) +export class RadicalGridComponent implements OnInit, AfterViewInit, OnDestroy { + + + constructor(private translate: TranslateService, + private route: ActivatedRoute, + private router: Router, + private characterService: CharacterService) {} + + public radicals: Radical[] = [] + public readonly radicalDisplayTreshold = 100; + @Output() radicalSelected = new EventEmitter(); + + private selected: Radical[] = []; + private _snackBar = inject(MatSnackBar); + private subscriptions: Subscription[] = []; + + public ngOnInit() { + this.showFirstNRadicals(); + } + + public ngAfterViewInit() { + this.subscriptions.push(this.route.queryParams + .subscribe(params => { + if (params['radicals']) { + this.radicalSelected.emit(true); + let radical = this.characterService.getRadical(params['radicals']); + if (radical) { + if (radical.index > this.radicalDisplayTreshold) { + this.showAllRadicals(); + } + this.selected = [radical]; + /** + let id = `radical-${radical.sign}`; + let el = document.getElementById(id); + if (el) { + el.scrollIntoView({behavior: 'smooth'}); + } + **/ + } + } + })); + } + + public toggleRadical(radical: Radical) { + this.selected = [radical]; + this.router.navigate( + ['/'], + { queryParams: { radicals: radical.sign, view: 'grid' }} + ); + } + + public toggleMultipleRadicals(radical: Radical) { + let index = this.selected.findIndex((r) => r == radical); + if (index >= 0) { + this.selected.splice(index, 1); + } else { + this.selected.push(radical); + } + if (this.selected.length > 0) { + let radicals = this.selected.map(c => c.sign).reduce((prev, current) => prev + ', ' + current); + let message = this.translate.instant('SNACKBAR.FIND_WORDS.MESSAGE', {radicals}); + let action = this.translate.instant('SNACKBAR.FIND_WORDS.ACTION'); + let snackBarRef = this._snackBar.open(message, action); + this.subscriptions.push(snackBarRef.onAction().subscribe(() => { + let radicals = this.selected.map(c => c.sign).reduce((prev, current) => prev + '&' + current); + this.router.navigate( + ['/'], + { queryParams: { radicals, view: 'grid' }} + ); + this.radicalSelected.emit(true); + })); + } + } + + public getTooltip(radical: Radical): string | undefined { + let lines: string[] = []; + if (radical.tooltip) { + let parts = radical.tooltip.split(':n='); + let translateKey = parts[0]; + let param = parts.length == 2 ? {n: +parts[1]}: undefined; + lines.push(this.translate.instant(translateKey, param)); + } + if (radical.compare) { + lines.push(`${this.translate.instant('TOOLTIP.COMPARE')} ${radical.compare}`); + } + if (radical.long) { + lines.push(`${this.translate.instant('TOOLTIP.SHORT_FORM')} ${radical.long}`); + } + if (radical.short) { + lines.push(`${this.translate.instant('TOOLTIP.LONG_FORM')} ${radical.short}`); + } + if (radical.original) { + lines.push(`${this.translate.instant('TOOLTIP.ORIGINAL')}: ${radical.original}`); + } + if (radical.variant) { + lines.push(`${this.translate.instant('TOOLTIP.VARIANT')}: ${radical.variant}`); + } + if (lines.length == 1) { + return lines[0]; + } else if (lines.length > 1) { + return lines.reduce((previous, current) => `${previous} \n${current}`); + } + return undefined; + } + + public isRadicalSelected(radical: Radical): boolean { + return this.selected.includes(radical); + } + + + public showAllRadicals() { + this.radicals = [...RADICALS]; + } + + public showFirstNRadicals() { + this.radicals = [...RADICALS].splice(0, this.radicalDisplayTreshold); + this.selected = this.selected.filter(r => this.radicals.includes(r)); + } + + public ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + +} diff --git a/src/app/components/radical-overview/radical-overview.component.css b/src/app/components/radical-overview/radical-overview.component.css new file mode 100644 index 0000000..b74e0c7 --- /dev/null +++ b/src/app/components/radical-overview/radical-overview.component.css @@ -0,0 +1,45 @@ +#radical-overview { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 95vw; + max-width: 95vw; + padding: 25px; +} + +.mat-container { + background-color: transparent; + color: inherit; + border-radius: 0; +} + +mat-drawer-container { + max-width: 100vw; +} + +app-radical-carousel { + width: 100vw; +} + +mat-sidenav { + width: 20vw; +} + +.reduced-width-72 { + width: 72vw !important; +} + +.reduced-width-70 { + width: 70vw !important; +} + +.example-left { + min-width: 150px; + padding-right: 10px; +} + +mat-button-toggle-group { + float: right; + transform: scaleY(85%); + margin-bottom: 10px; +} diff --git a/src/app/components/radical-overview/radical-overview.component.html b/src/app/components/radical-overview/radical-overview.component.html new file mode 100644 index 0000000..c3e17d3 --- /dev/null +++ b/src/app/components/radical-overview/radical-overview.component.html @@ -0,0 +1,44 @@ +
 
+ + + + +
+
+

{{"RADICAL_OVERVIEW.TITLE" | translate }}

+

{{"RADICAL_OVERVIEW.INTRO" | translate}}

+

{{"RADICAL_OVERVIEW.OVERVIEW" | translate}}

+ + + view_carousel {{"VIEW.SINGLE" | translate }} + grid_on {{"VIEW.GRID" | translate }} + +
+ + +
+ +
+
+ + {{"RADICAL_OVERVIEW.HINT" | translate}} +
+ +
+

{{"RADICAL_OVERVIEW.HINT2" | translate}}

+ +
+ {{"RADICAL_OVERVIEW.EXAMPLE_1.TITLE" | translate}} + {{"RADICAL_OVERVIEW.EXAMPLE_1.SUB" | translate}} +
+ +
+ {{"RADICAL_OVERVIEW.EXAMPLE_2.TITLE" | translate}} + {{"RADICAL_OVERVIEW.EXAMPLE_2.SUB" | translate}} +
+
+ +
+
+ + diff --git a/src/app/components/radical-overview/radical-overview.component.spec.ts b/src/app/components/radical-overview/radical-overview.component.spec.ts new file mode 100644 index 0000000..ecd2ddb --- /dev/null +++ b/src/app/components/radical-overview/radical-overview.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RadicalOverviewComponent } from './radical-overview.component'; + +describe('RadicalOverviewComponent', () => { + let component: RadicalOverviewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RadicalOverviewComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RadicalOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/radical-overview/radical-overview.component.ts b/src/app/components/radical-overview/radical-overview.component.ts new file mode 100644 index 0000000..d50abdb --- /dev/null +++ b/src/app/components/radical-overview/radical-overview.component.ts @@ -0,0 +1,64 @@ +import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {TranslateService} from "@ngx-translate/core"; +import {Subscription} from "rxjs"; +import {ActivatedRoute, Router} from "@angular/router"; +import {CharacterService} from "../../services/character.service"; + +@Component({ + selector: 'app-radical-overview', + templateUrl: './radical-overview.component.html', + styleUrl: './radical-overview.component.css' +}) +export class RadicalOverviewComponent implements OnInit, AfterViewInit, OnDestroy { + view: 'single' | 'grid' = 'grid'; + @ViewChild('drawer') characterSidecar: any; + private subscriptions: Subscription[] = []; + constructor(private translate: TranslateService, + private route: ActivatedRoute, + private router: Router, + private characterService: CharacterService) {} + + public ngOnInit() { + this.subscriptions.push(this.route.queryParams + .subscribe(params => { + if (params['view']) { + this.view = params['view']; + } + + if (params['radicals']) { + let el = document.getElementById('radical-overview-top'); + if (el) { + el.scrollIntoView({behavior: 'smooth', block: "start"}); + } + } + })); + } + + public ngAfterViewInit() { // FIXME ExpressionChangedAfterItHasBeenCheckedError + this.subscriptions.push(this.route.queryParams + .subscribe(params => { + if (params['radicals']) { + if (this.characterSidecar) { + this.characterSidecar.open(); + } + } + })); + } + + public changeView(view: 'single' | 'grid') { + this.view = view; + let radicals = this.route.snapshot.queryParams['radicals']; + if (!radicals && view == 'single') { + radicals = this.characterService.getRandomRadical().sign; + } + this.router.navigate( + ['/'], + { queryParams: { radicals, view }} + ); + } + + public ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + +} diff --git a/src/app/components/toolbar/toolbar.component.css b/src/app/components/toolbar/toolbar.component.css new file mode 100644 index 0000000..7fc46cf --- /dev/null +++ b/src/app/components/toolbar/toolbar.component.css @@ -0,0 +1,21 @@ +mat-toolbar { + justify-content: space-between; + position: sticky; + position: -webkit-sticky; + top: 0; + z-index: 1000; + background-color: darkviolet; +} + +img { + top: 5px; + max-height: 35px; +} + +.title-link:hover { + cursor: pointer +} + +button { + transform: translateY(5px); +} diff --git a/src/app/components/toolbar/toolbar.component.html b/src/app/components/toolbar/toolbar.component.html new file mode 100644 index 0000000..33d2ee5 --- /dev/null +++ b/src/app/components/toolbar/toolbar.component.html @@ -0,0 +1,22 @@ + + + +
+ + + + + + + + +
+
diff --git a/src/app/components/toolbar/toolbar.component.spec.ts b/src/app/components/toolbar/toolbar.component.spec.ts new file mode 100644 index 0000000..5b58ba7 --- /dev/null +++ b/src/app/components/toolbar/toolbar.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToolbarComponent } from './toolbar.component'; + +describe('ToolbarComponent', () => { + let component: ToolbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ToolbarComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ToolbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/toolbar/toolbar.component.ts b/src/app/components/toolbar/toolbar.component.ts new file mode 100644 index 0000000..1d9a857 --- /dev/null +++ b/src/app/components/toolbar/toolbar.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import {TranslateService} from "@ngx-translate/core"; + +@Component({ + selector: 'app-toolbar', + templateUrl: './toolbar.component.html', + styleUrl: './toolbar.component.css' +}) +export class ToolbarComponent { + + constructor(private translate: TranslateService) { + } + + public switchLanguage(lang: string) { + this.translate.use(lang); + } + +} diff --git a/src/app/components/topic-banner/topic-banner.component.css b/src/app/components/topic-banner/topic-banner.component.css new file mode 100644 index 0000000..f4c1506 --- /dev/null +++ b/src/app/components/topic-banner/topic-banner.component.css @@ -0,0 +1,32 @@ + +.topic-banner { + padding-bottom: 10px; +} + +.topic-banner:hover .scroll { + animation-play-state: paused; +} + + +.topic-banner .scroll { + will-change: transform; +} + +.reverse .scroll { + animation-direction: reverse; +} + +.overlay { + transform: translateX(-100%); +} + + +@keyframes loop { + 0% {transform: translateX(0);} + 100% {transform: translateX(-100%);} +} + + +button { + margin: 10px; +} diff --git a/src/app/components/topic-banner/topic-banner.component.html b/src/app/components/topic-banner/topic-banner.component.html new file mode 100644 index 0000000..d58a94b --- /dev/null +++ b/src/app/components/topic-banner/topic-banner.component.html @@ -0,0 +1,35 @@ +
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+
diff --git a/src/app/components/topic-banner/topic-banner.component.spec.ts b/src/app/components/topic-banner/topic-banner.component.spec.ts new file mode 100644 index 0000000..fba871d --- /dev/null +++ b/src/app/components/topic-banner/topic-banner.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TopicBannerComponent } from './topic-banner.component'; + +describe('TopicBannerComponent', () => { + let component: TopicBannerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TopicBannerComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TopicBannerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/topic-banner/topic-banner.component.ts b/src/app/components/topic-banner/topic-banner.component.ts new file mode 100644 index 0000000..b8014e3 --- /dev/null +++ b/src/app/components/topic-banner/topic-banner.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-topic-banner', + templateUrl: './topic-banner.component.html', + styleUrl: './topic-banner.component.css' +}) +export class TopicBannerComponent { + + + public topics = ['FRUITS', 'DRINKS', 'PERSONAL_PRONOUNS', 'QUESTION_WORDS', 'WEATHER', 'FAMILY', 'NATURE']; + +} diff --git a/src/app/data/combinations.ts b/src/app/data/combinations.ts new file mode 100644 index 0000000..d5968f8 --- /dev/null +++ b/src/app/data/combinations.ts @@ -0,0 +1,17 @@ +import {Word, WORD_TYPE} from "../interfaces/word.data"; + +export const COMBINATIONS: Word[] = [ + { hanzi:'母', pinyin: 'mǔ', composition: [['乙', '𠃌', '丶', '一', '丶']], translationKey: 'mother', type: [WORD_TYPE.NOUN, WORD_TYPE.COMB_EL]}, + { hanzi:'莓', pinyin: 'méi', composition: [['艹', '𠂉', '母']], translationKey: 'FRUITS.RASPBERRY' , type: [WORD_TYPE.NOUN, WORD_TYPE.COMB_EL]}, + { hanzi:'丁', pinyin: 'dīng', composition: [['一', '丿']], translationKey: 'man', type: [WORD_TYPE.NOUN, WORD_TYPE.COMB_EL]}, + { hanzi:'兆', pinyin: 'zhào', composition: [['冫', '儿', '丶', '丶']], translationKey: 'trillion, good news, omen, sign', type: [WORD_TYPE.NOUN, WORD_TYPE.COMB_EL]}, + { hanzi:'桃', pinyin: 'táo', composition: [['木', '兆']], translationKey: 'FRUITS.PEACH', type: [WORD_TYPE.NOUN, WORD_TYPE.COMB_EL], emoji: '🍑' }, + { hanzi:'甫', pinyin: 'fǔ', composition: [['十', '丶', '月']], translationKey: 'just now', type: [WORD_TYPE.PHRASE, WORD_TYPE.COMB_EL] }, + { hanzi:'丩', pinyin: 'jiū', composition: [['乙', '丨']], translationKey: 'entanglement', type: [WORD_TYPE.NOUN, WORD_TYPE.COMB_EL] }, + { hanzi:'生', pinyin: 'shēng', composition: [['𠂉', '土']], translationKey: 'born', type: [WORD_TYPE.ADJECTIVE, WORD_TYPE.COMB_EL] }, + { hanzi:'果', pinyin: 'guǒ', composition: [['田', '木']], translationKey: 'FRUITS.FRUIT', type: [WORD_TYPE.NOUN, WORD_TYPE.COMB_EL] }, + { hanzi:'好', pinyin: 'hǎo', composition: [['女', '子']], translationKey: 'good', type: [WORD_TYPE.ADJECTIVE, WORD_TYPE.COMB_EL] }, + { hanzi:'国', pinyin: 'guó', composition: [['囗', '玉']], translationKey: 'country', type: [WORD_TYPE.NOUN, WORD_TYPE.COMB_EL] }, + { hanzi: '家', pinyin: 'jiā', composition: [['宀', '豕']], translationKey: "family, home", type: [WORD_TYPE.NOUN, WORD_TYPE.COMB_EL] }, + { hanzi: '你', pinyin: 'nǐ', composition: [['亻', '𠂉', '小']], translationKey: "PERSONAL_PRONOUNS.YOU", type: [WORD_TYPE.PRONOUN, WORD_TYPE.COMB_EL] }, +]; diff --git a/src/app/data/earth.branches.ts b/src/app/data/earth.branches.ts new file mode 100644 index 0000000..3842017 --- /dev/null +++ b/src/app/data/earth.branches.ts @@ -0,0 +1,32 @@ +export enum SEASON { WINTER, SPRING, SUMMER, AUTUMN } + +// https://de.wikipedia.org/wiki/Erdzweige +export const EARTH_BRANCHES = [ + { sign:'子', pinyin: 'zǐ', animal: '鼠', direction: 0, season: SEASON.WINTER, month: 11, hour: 0, radicalIndex: 74 }, + { sign:'丑', pinyin: 'chǒu', animal: '牛', direction: 30, season: SEASON.WINTER, month: 12, hour: 2, meaning: 'ugly, hideous' }, + { sign:'寅', pinyin: 'yín', animal: '虎', direction: 60, season: SEASON.SPRING, month: 1, hour: 4 }, + { sign:'卯', pinyin: 'mǎo', animal: '兔', direction: 90, season: SEASON.SPRING, month: 2, hour: 6 }, + { sign:'辰', pinyin: 'chén', animal: '龍', direction: 120, season: SEASON.SPRING, month: 3, hour: 8, radicalIndex: 187 }, + { sign:'巳', pinyin: 'sì', animal: '蛇', direction: 150, season: SEASON.SUMMER, month: 4, hour: 10 }, + { sign:'午', pinyin: 'wǔ', animal: '馬', direction: 180, season: SEASON.SUMMER, month: 5, hour: 12, meaning: 'midday, noon' }, + { sign:'未', pinyin: 'wèi', animal: '羊', direction: 210, season: SEASON.SUMMER, month: 6, hour: 14, meaning: 'not yet' }, + { sign:'申', pinyin: 'shēn', animal: '猴', direction: 240, season: SEASON.AUTUMN, month: 7, hour: 16, radicalIndex: 144 }, + { sign:'酉', pinyin: 'yǒu', animal: '雞', direction: 270, season: SEASON.AUTUMN, month: 8, hour: 18, radicalIndex: 193 }, + { sign:'戌', pinyin: 'xū', animal: '狗', direction: 300, season: SEASON.AUTUMN, month: 9, hour: 20 }, + { sign:'亥', pinyin: 'hài', animal: '豬', direction: 330, season: SEASON.WINTER, month: 10, hour: 22 }, +]; + +export const ANIMALS = [ + {sign: '鼠', pinyin: 'shǔ', meaning: 'Ratte'}, //225 + {sign: '牛', pinyin: 'niú', meaning: 'Büffel'}, //110 + {sign: '虎', pinyin: 'hǔ', meaning: 'Tiger'}, // 173 + {sign: '兔', pinyin: 'tù', meaning: 'Hase'}, + {sign: '龍', pinyin: 'lóng', meaning: 'Drache'}, //137 + {sign: '蛇', pinyin: 'shé', meaning: 'Schlange'}, + {sign: '馬', pinyin: 'mǎ', meaning: 'Pferd'}, // 75 + {sign: '羊', pinyin: 'yáng', meaning: 'Schaf'}, // 157 + {sign: '猴', pinyin: 'hóu', meaning: 'Affe'}, + {sign: '雞', pinyin: 'jī', meaning: 'Hahn'}, + {sign: '狗', pinyin: 'gǒu', meaning: 'Hund'}, + {sign: '豬', pinyin: 'zhū', meaning: 'Schwein'} //194 +]; diff --git a/src/app/data/heaven.stems.ts b/src/app/data/heaven.stems.ts new file mode 100644 index 0000000..66d336d --- /dev/null +++ b/src/app/data/heaven.stems.ts @@ -0,0 +1,24 @@ + + +export const phases = [ + {sign: '木', radicalIndex: 94}, + {sign: '火', radicalIndex: 83}, + {sign: '土', radicalIndex: 49}, + {sign: '金', radicalIndex: 209}, + {sign: '水', radicalIndex: 125} +]; + + +// https://de.wikipedia.org/wiki/Himmelsst%C3%A4mme +export const HEAVENLY_STEMS = [ + { sign:'甲', pinyin: 'jiǎ', yin: false, phase: '木', radicalIndex: 232}, + { sign:'乙', pinyin: 'yǐ', yin: true, phase: '木', radicalIndex: 7}, + { sign:'丙', pinyin: 'bǐng', yin: false, phase: '火'}, + { sign:'丁', pinyin: 'dīng', yin: true, phase: '火', meaning: 'man'}, + { sign:'戊', pinyin: 'wù', yin: false, phase: '土', radicalIndex: 138}, + { sign:'己', pinyin: 'jǐ', yin: true, phase: '土', radicalIndex: 72}, + { sign:'庚', pinyin: 'gēng', yin: false, phase: '金'}, + { sign:'辛', pinyin: 'xīn', yin: true, phase: '金', radicalIndex: 186}, + { sign:'壬', pinyin: 'rén', yin: false, phase: '水'}, + { sign:'癸', pinyin: 'guǐ', yin: true, phase: '水'}, +]; diff --git a/src/app/data/radicals.ts b/src/app/data/radicals.ts new file mode 100644 index 0000000..ec0843d --- /dev/null +++ b/src/app/data/radicals.ts @@ -0,0 +1,239 @@ +import {Radical} from "../interfaces/radical.data"; + +// https://de.wikipedia.org/wiki/227_Radikale +export const RADICALS: Radical[] = [ + { index: 1, sign: '丶', strokes: 1, translationKey: 'Punkt', pinyin: 'diǎn, zhǔ'}, + { index: 2, sign: '一', strokes: 1, translationKey: 'eins', pinyin: 'yī'}, + { index: 3, sign: '丨', strokes: 1, translationKey: 'Senkrechtstrich', pinyin: 'shù'}, + { index: 4, sign: '丿', strokes: 1, translationKey: 'Schrägstrich', pinyin: 'piě'}, + { index: 5, sign: '乛', strokes: 1, translationKey: '', pinyin: 'hénggōu'}, + { index: 6, sign: '𠃌', strokes: 1, translationKey: '', pinyin: 'héngpiě'}, + { index: 8, sign: '冫', strokes: 2, translationKey: 'Eis', long:'125 shuǐ 水', pinyin: '', emoji: '🧊'}, + { index: 7, sign: '乙', strokes: 1, translationKey: '', tooltip: 'TOOLTIP.N_TH_HEAVEN:n=2', pinyin: 'yǐ', emoji: '☁️'}, + { index: 9, sign: '亠', strokes: 2, translationKey: 'Deckel', pinyin: 'tóu'}, + { index: 10, sign: '讠', strokes: 2, translationKey: 'sprechen', long: '185 xīn 言', pinyin: '', emoji: '💬'}, + { index: 11, sign: '二', strokes: 2, translationKey: 'zwei', pinyin: 'èr'}, + { index: 12, sign: '十', strokes: 2, translationKey: 'zehn', pinyin: 'shí'}, + { index: 13, sign: '厂', strokes: 2, translationKey: 'Abhang, Fabrik', pinyin: 'cháng'}, + { index: 14, sign: '𠂇', strokes: 2, translationKey: '', pinyin: 'zuǒ'}, + { index: 15, sign: '匚', strokes: 2, translationKey: 'Rahmen', pinyin: 'fāng'}, + { index: 16, sign: '卜', strokes: 2, translationKey: 'Orakel, wahrsagen', pinyin: 'bǔ', emoji: '🔮'}, + { index: 17, sign: '刂', strokes: 2, translationKey: 'Messer', long: '27 dāo 刀', pinyin: 'dāo', emoji: '🔪'}, + { index: 18, sign: '冖', strokes: 2, translationKey: 'Deckel, Krone', compare: '45 宀 Dach', pinyin: 'mì', emoji: '👑'}, + { index: 19, sign: '冂', strokes: 2, translationKey: 'Begrenzung', pinyin: 'mì'}, + { index: 20, sign: '𠂉', strokes: 2, translationKey: 'Begrenzung', pinyin: 'mì'}, + { index: 21, sign: '亻', strokes: 2, translationKey: 'Mensch', long: '23 rén 人', pinyin: '', emoji: '🚶'}, + { index: 22, sign: '⺁', strokes: 2, translationKey: '', pinyin: ''}, + { index: 23, sign: '人', strokes: 2, translationKey: 'Mensch', short: '21 亻', pinyin: 'rén', emoji: '🚶'}, + { index: 24, sign: '八', strokes: 2, translationKey: 'acht', pinyin: 'bā'}, + { index: 25, sign: '乂', strokes: 2, translationKey: '', pinyin: ''}, + { index: 26, sign: '勹', strokes: 2, translationKey: 'einpacken, Rahmen', pinyin: ''}, + { index: 27, sign: '刀', strokes: 2, translationKey: 'Messer, Schwert', short: '17 刂', pinyin: 'dāo', emoji: '🔪'}, + { index: 28, sign: '力', strokes: 2, translationKey: 'Kraft', pinyin: 'lì', emoji:'⚡'}, + { index: 29, sign: '儿', strokes: 2, translationKey: 'Beine, Kind', pinyin: 'ér', emoji:'🦵'}, + { index: 30, sign: '几', strokes: 2, translationKey: 'Tischchen', pinyin: 'jī'}, + { index: 31, sign: '龴', strokes: 2, translationKey: '', pinyin: ''}, + { index: 32, sign: '卩', strokes: 2, translationKey: 'Siegel', pinyin: 'jié'}, + { index: 33, sign: '⻖', strokes: 2, translationKey: 'Hügel, linkes Ohr', original: '阜', pinyin: '', emoji:'👂'}, + { index: 34, sign: '⻏', strokes: 2, translationKey: 'Dorf, Stadt, rechtes Ohr', original: '邑', pinyin: '', emoji:'👂'}, + { index: 35, sign: '又', strokes: 2, translationKey: 'rechte Hand; auch, wieder', pinyin: 'yòu', emoji:'✋'}, + { index: 36, sign: '廴', strokes: 2, translationKey: 'marschieren', pinyin: 'yín'}, + { index: 37, sign: '厶', strokes: 2, translationKey: 'Kokon, privat, persönlich', pinyin: 'sī', emoji:'🐛'}, + { index: 38, sign: '凵', strokes: 2, translationKey: 'Schüssel, offener Mund', pinyin: 'qǔ'}, + { index: 39, sign: '匕', strokes: 2, translationKey: 'Löffel', pinyin: 'bǐ', emoji:'🥄'}, + { index: 40, sign: '氵', strokes: 3, translationKey: 'Wasser', long: '125 shuǐ 水', pinyin: '', emoji:'💧'}, + { index: 41, sign: '忄', strokes: 3, translationKey: 'Herz', long: '81 xīn 心', pinyin: '', emoji:'🫀'}, + { index: 42, sign: '丬', strokes: 3, translationKey: 'gespaltener Bambus', long: 'jiàng 爿', pinyin: 'pán', emoji:'🎋'}, + { index: 43, sign: '亡', strokes: 3, translationKey: 'sterben, fliehen, flüchten', pinyin: 'wáng', emoji:'⚰️'}, + { index: 44, sign: '广', strokes: 3, translationKey: 'weit, breit, ausgedehnt', compare: '127 疒 krank', pinyin: 'guǎng'}, + { index: 45, sign: '宀', strokes: 3, translationKey: 'Dach, Deckel', compare: '18 mì 冖 Deckel, Krone', pinyin: ''}, + { index: 46, sign: '门', strokes: 3, translationKey: 'Tür, Tor', original: '門', pinyin: 'mén', emoji:'🚪'}, + { index: 47, sign: '辶', strokes: 3, translationKey: 'anhalten', long: '189 zǒu 走', original:'辵', pinyin: ''}, + { index: 48, sign: '工', strokes: 3, translationKey: 'Arbeit, arbeiten', pinyin: 'gōng'}, + { index: 49, sign: '土', strokes: 3, translationKey: 'Erde', tooltip: 'TOOLTIP.N_TH_ELEMENT:n=3', pinyin: 'tǔ', emoji:'🪨'}, + { index: 50, sign: '艹', strokes: 3, translationKey: 'Gras', original: '艸', pinyin: '', emoji:'🌱'}, + { index: 51, sign: '廾', strokes: 3, translationKey: 'erhobene Hände', pinyin: 'yì', emoji:'🙌'}, + { index: 52, sign: '大', strokes: 3, translationKey: 'groß', pinyin: 'dà'}, + { index: 53, sign: '尢', strokes: 3, translationKey: 'lahm', compare: '29 儿 Beine', pinyin: 'yóu'}, + { index: 54, sign: '寸', strokes: 3, translationKey: 'Daumen, Zoll', pinyin: 'cùn', emoji:'👍'}, + { index: 55, sign: '扌', strokes: 3, translationKey: 'Hand', long: '111 shǒu 手', pinyin: '', emoji:'✋'}, + { index: 56, sign: '弋', strokes: 3, translationKey: 'Wurfspieß, Pfeil mit Schnur', compare: '101 gē 戈 Lanze', pinyin: 'yì', emoji:'🏹'}, + { index: 57, sign: '巾', strokes: 3, translationKey: 'Tuch, Schal', pinyin: 'jīn', emoji:'🧣'}, + { index: 58, sign: '口', strokes: 3, translationKey: 'Mund, Öffnung', pinyin: 'kǒu', emoji:'👄'}, + { index: 59, sign: '囗', strokes: 3, translationKey: 'umgeben, Rahmen', pinyin: ''}, + { index: 60, sign: '山', strokes: 3, translationKey: 'Berg, Gebirge', pinyin: 'shān', emoji:'⛰️'}, + { index: 61, sign: '屮', strokes: 3, translationKey: 'Spross, Trieb, junges Gras', pinyin: 'chè', emoji:'🌱'}, + { index: 62, sign: '彳', strokes: 3, translationKey: 'Schritt, schlendern, bummeln', pinyin: '', emoji:'👣'}, + { index: 63, sign: '彡', strokes: 3, translationKey: 'Haarsträne', compare: '220 biāo 髟 Haar', pinyin: '', emoji:'💇‍'}, + { index: 64, sign: '夕', strokes: 3, translationKey: 'Dämmerung, Sonnenuntergang', pinyin: 'xī', emoji:'🌇'}, + { index: 65, sign: '夂', strokes: 3, translationKey: 'verfolgen, langsam', pinyin: 'zhǐ'}, + { index: 66, sign: '丸', strokes: 3, translationKey: 'Kugel, Ball, Pille', pinyin: 'wán'}, + { index: 67, sign: '尸', strokes: 3, translationKey: 'Leichnam', pinyin: 'shī', emoji:'💀'}, + { index: 68, sign: '饣', strokes: 3, translationKey: 'Nahrung', long: '217 shí 食', pinyin: '', emoji:'🥘'}, + { index: 69, sign: '犭', strokes: 3, translationKey: 'Hund', long: '96 quǎn 犬', pinyin: '', emoji:'🐕'}, + { index: 70, sign: '彐', strokes: 3, translationKey: 'Schweinerüssel', variant: '彑', pinyin: 'jì', emoji:'🐽'}, + { index: 71, sign: '弓', strokes: 3, translationKey: 'Bogen', pinyin: 'gōng', emoji:'🏹'}, + { index: 72, sign: '己', strokes: 3, translationKey: 'selbst, persönlich, bereits', tooltip: 'TOOLTIP.N_TH_HEAVEN:n=6', pinyin: 'jǐ', emoji: '☁️'}, + { index: 73, sign: '女', strokes: 3, translationKey: 'Frau', pinyin: 'nǚ', emoji:'👩'}, + { index: 74, sign: '子', strokes: 3, translationKey: 'Kind', tooltip: 'TOOLTIP.N_TH_EARTH:n=1', pinyin: 'zǐ', emoji:'🧒'}, + { index: 75, sign: '马', strokes: 3, translationKey: 'Pferd', original: '馬', pinyin: 'mǎ', emoji:'🐎'}, + { index: 76, sign: '幺', strokes: 3, translationKey: 'eins', pinyin: 'yāo'}, + { index: 77, sign: '纟', strokes: 3, translationKey: 'Seide', original: '糸 & 糹', pinyin: 'mì'}, + { index: 78, sign: '巛', strokes: 3, translationKey: 'Strom, Fluss', pinyin: 'chuān'}, + { index: 79, sign: '小', strokes: 3, translationKey: 'klein', variant: '⺌', pinyin: 'xiǎo'}, + { index: 80, sign: '灬', strokes: 4, translationKey: 'Feuer', long: '83 huǒ 火', pinyin: '', emoji:'🔥'}, + { index: 81, sign: '心', strokes: 4, translationKey: 'Herz', short: '41 忄', pinyin: 'xīn', emoji:'🫀'}, + { index: 82, sign: '斗', strokes: 4, translationKey: 'Scheffel, Hohlmaß', pinyin: 'dòu'}, + { index: 83, sign: '火', strokes: 4, translationKey: 'Feuer, Flamme', short:'80 灬', tooltip: 'TOOLTIP.N_TH_ELEMENT:n=2', pinyin: 'huǒ', emoji:'🔥'}, + { index: 84, sign: '文', strokes: 4, translationKey: 'Muster, Kultur, Schrift, Text', pinyin: 'wén'}, + { index: 85, sign: '方', strokes: 4, translationKey: 'Viereck, Himmelsrichtung', pinyin: 'fāng'}, + { index: 86, sign: '户', strokes: 4, translationKey: 'Türflügel', compare: '67 shī 尸 Leichnam', pinyin: 'hù', emoji:'🚪'}, + { index: 87, sign: '礻', strokes: 4, translationKey: 'Hinweis', long: '132 shì 示', pinyin: ''}, + { index: 88, sign: '王', strokes: 4, translationKey: 'König', pinyin: 'wáng', emoji:'🫅'}, + { index: 89, sign: '主', strokes: 4, translationKey: 'Herrscher, Gastgeber', compare: '88 wáng 王 König', pinyin: 'zhǔ'}, + { index: 90, sign: '天', strokes: 4, translationKey: 'Himmel, Tag', variant: '夭', pinyin: 'tiān', emoji:'☁️'}, + { index: 91, sign: '韦', strokes: 4, translationKey: 'Leder', pinyin: 'wéi'}, + { index: 92, sign: '耂', strokes: 4, translationKey: 'alt', original: '老', pinyin: 'lǎo'}, + { index: 93, sign: '廿', strokes: 4, translationKey: 'zwanzig', pinyin: 'niàn'}, + { index: 94, sign: '木', strokes: 4, translationKey: 'Baum, Holz', tooltip: 'TOOLTIP.N_TH_ELEMENT:n=1', variant:'朩', pinyin: 'mù', emoji:'🪵'}, + { index: 95, sign: '不', strokes: 4, translationKey: 'nicht', pinyin: 'bù'}, + { index: 96, sign: '犬', strokes: 4, translationKey: 'Hund', short: '69 犭', pinyin: 'quǎn', emoji:'🐕'}, + { index: 97, sign: '歹', strokes: 4, translationKey: 'schlecht, böse', pinyin: 'dǎi'}, + { index: 98, sign: '瓦', strokes: 4, translationKey: 'Ziegel', pinyin: 'wǎ'}, + { index: 99, sign: '牙', strokes: 4, translationKey: 'Eckzahn', compare: '206 齿 Backenzahn', pinyin: 'yá', emoji:'🦷'}, + { index: 100, sign: '车', strokes: 4, translationKey: 'Wagen, Fahrzeug', original: '車', pinyin: 'chē', emoji:'🚗'}, + { index: 101, sign: '戈', strokes: 4, translationKey: 'Lanze, Hiebaxt', compare: '56 yì 弋 Wurfspieß', pinyin: 'gē', emoji:'🪓'}, + { index: 102, sign: '止', strokes: 4, translationKey: 'Zehe, stehen bleiben, unterbrechen', pinyin: 'zhǐ', emoji:'🦶'}, + { index: 103, sign: '日', strokes: 4, translationKey: 'Sonne', pinyin: 'rì', emoji:'☀️'}, + { index: 104, sign: '曰', strokes: 4, translationKey: 'sprechen, sagen', pinyin: 'yuē', emoji:'💬'}, + { index: 105, sign: '中', strokes: 4, translationKey: 'Mitte', pinyin: 'zhōng'}, + { index: 106, sign: '贝', strokes: 4, translationKey: 'Kaurimuschel', original: '貝', pinyin: 'bèi', emoji:'🐚'}, + { index: 107, sign: '见', strokes: 4, translationKey: 'sehen', original: '見', pinyin: 'jiàn', emoji:'👁️'}, + { index: 108, sign: '父', strokes: 4, translationKey: 'Vater', pinyin: 'fù'}, + { index: 109, sign: '气', strokes: 4, translationKey: 'Dampf, Gas, Luft', pinyin: 'qì'}, + { index: 110, sign: '牛', strokes: 4, translationKey: 'Büffel, Rind', pinyin: 'niú', emoji:'🐄'}, + { index: 111, sign: '手', strokes: 4, translationKey: 'Hand', short: '55 扌', pinyin: 'shǒu', emoji:'🐄'}, + { index: 112, sign: '毛', strokes: 4, translationKey: 'Haar, Borste', pinyin: 'máo'}, + { index: 113, sign: '攵', strokes: 4, translationKey: 'klopfen', original: '攴', pinyin: 'pū'}, + { index: 114, sign: '片', strokes: 4, translationKey: 'Scheibe, Platte', pinyin: 'piàn'}, + { index: 115, sign: '斤', strokes: 4, translationKey: 'Axt, Pfund', pinyin: 'jīn', emoji:'🪓'}, + { index: 116, sign: '爪', strokes: 4, translationKey: 'Klaue', variant: '爫', pinyin: 'zhuā'}, + { index: 117, sign: '尺', strokes: 4, translationKey: 'Fuß (Längenmaß)', compare: '67 shī 尸 Leichnam', pinyin: 'chǐ', emoji:'🦶'}, + { index: 118, sign: '月', strokes: 4, translationKey: 'Mond, Monat', pinyin: 'yuè', emoji:'🌙'}, + { index: 119, sign: '殳', strokes: 4, translationKey: 'Keule, Lanze aus Bambus', pinyin: 'shū'}, + { index: 120, sign: '欠', strokes: 4, translationKey: 'gähnen, schulden, mangeln', pinyin: 'qiàn'}, + { index: 121, sign: '风', strokes: 4, translationKey: 'Wind', original: '風', pinyin: 'fēng', emoji:'🌬️'}, + { index: 122, sign: '氏', strokes: 4, translationKey: 'Sippe, Familie', compare: '233 mín 民 Volk', pinyin: 'shì'}, + { index: 123, sign: '比', strokes: 4, translationKey: 'vergleichen', compare: '39 bǐ 匕 Löffel', pinyin: 'bǐ'}, + { index: 124, sign: '肀', strokes: 4, translationKey: 'Pinsel', variant: '⺻', pinyin: '', emoji:'🖌️'}, + { index: 125, sign: '水', strokes: 4, translationKey: 'Wasser', short: '8 & 40 氵', tooltip: 'TOOLTIP.N_TH_ELEMENT:n=5', pinyin: 'shuǐ', emoji:'🌊'}, + { index: 126, sign: '立', strokes: 5, translationKey: 'stehen, aufrichten', pinyin: 'lì'}, + { index: 127, sign: '疒', strokes: 5, translationKey: 'Krankheit', long: 'bìng 病', pinyin: '', emoji:'🤒'}, + { index: 128, sign: '穴', strokes: 5, translationKey: 'Höhle', pinyin: 'xuè'}, + { index: 129, sign: '衤', strokes: 5, translationKey: 'Kleidung', long: '161 qí 衣', pinyin: 'yī', emoji:'👕'}, + { index: 130, sign: '𡗗', strokes: 5, translationKey: '', pinyin: ''}, + { index: 131, sign: '玉', strokes: 5, translationKey: 'Jade', compare: '88 wáng 王 König', pinyin: 'yù'}, + { index: 132, sign: '示', strokes: 5, translationKey: 'Hinweis, Zeichen', short: '87 礻', pinyin: 'shì'}, + { index: 133, sign: '去', strokes: 5, translationKey: 'weg gehen', pinyin: 'qù'}, + { index: 134, sign: '𤇾', strokes: 5, translationKey: '', pinyin: ''}, + { index: 135, sign: '甘', strokes: 5, translationKey: 'süß', pinyin: 'gān'}, + { index: 136, sign: '石', strokes: 5, translationKey: 'Stein', pinyin: 'shí', emoji:'🪨'}, + { index: 137, sign: '龙', strokes: 5, translationKey: 'Drache', original: '龍', pinyin: 'lóng', emoji:'🐉'}, + { index: 138, sign: '戊', strokes: 5, translationKey: '', tooltip: 'TOOLTIP.N_TH_HEAVEN:n=5', pinyin: 'wù', emoji:'☁️'}, + { index: 139, sign: '龸', strokes: 5, translationKey: '', compare: '45 宀 Dach', pinyin: ''}, + { index: 140, sign: '业', strokes: 5, translationKey: 'Geschäft, Branche', pinyin: 'yè'}, + { index: 141, sign: '目', strokes: 5, translationKey: 'Auge', pinyin: 'mù', emoji:'👁️'}, + { index: 142, sign: '田', strokes: 5, translationKey: 'Feld', pinyin: 'tián'}, + { index: 143, sign: '由', strokes: 5, translationKey: 'Grund, Ursache, wegen, durch', pinyin: 'yóu'}, + { index: 144, sign: '申', strokes: 5, translationKey: 'erklären', tooltip:'TOOLTIP.N_TH_EARTH:n=9', pinyin: 'shēn'}, + { index: 145, sign: '罒', strokes: 5, translationKey: 'Netz', pinyin: 'shēn', emoji:'🕸️'}, + { index: 146, sign: '皿', strokes: 5, translationKey: 'Schüssel, Geschirr', compare: '181 xuè 血 Blut', pinyin: 'mǐn', emoji:'🥣'}, + { index: 147, sign: '钅', strokes: 5, translationKey: 'Gold, Metall', long: '209 jīn 金', pinyin: 'jīn'}, + { index: 148, sign: '矢', strokes: 5, translationKey: 'Pfeil', pinyin: 'shǐ', emoji:'🏹'}, + { index: 149, sign: '禾', strokes: 5, translationKey: 'Getreide', compare: '94 mù 木 Baum', pinyin: 'hé', emoji:'🌾'}, + { index: 150, sign: '白', strokes: 5, translationKey: 'weiß', compare: '103 rì 日 Sonne', pinyin: 'bái', emoji:'🏳️'}, + { index: 151, sign: '瓜', strokes: 5, translationKey: 'Melone', pinyin: 'guā', emoji:'🍈'}, + { index: 152, sign: '鸟', strokes: 5, translationKey: 'Vogel', original: '鳥', pinyin: 'niǎo', emoji:'🐓'}, + { index: 153, sign: '皮', strokes: 5, translationKey: 'Haut, Leder', pinyin: 'pí'}, + { index: 154, sign: '癶', strokes: 5, translationKey: 'Rücken', pinyin: 'bō'}, + { index: 155, sign: '矛', strokes: 5, translationKey: 'Speer, Lanze', pinyin: 'máo'}, + { index: 156, sign: '疋', strokes: 5, translationKey: 'Stoffballen', pinyin: 'shū, pǐ'}, + { index: 157, sign: '羊', strokes: 6, translationKey: 'Ziege, Schaf', variant: '⺷', pinyin: 'yáng', emoji:'🐑'}, + { index: 158, sign: '龹', strokes: 6, translationKey: 'Rolle', pinyin: 'shū, pǐ'}, + { index: 159, sign: '米', strokes: 6, translationKey: 'Reis', pinyin: 'mǐ', emoji:'🍚'}, + { index: 160, sign: '齐', strokes: 6, translationKey: 'aufreihen, ordentlich, gleichmäßig', original: '齊', pinyin: 'qí'}, + { index: 161, sign: '衣', strokes: 6, translationKey: 'Kleidung, Kleid', short: '129 衤', pinyin: 'qí', emoji:'👗'}, + { index: 162, sign: '亦', strokes: 6, translationKey: 'auch, ebenso', pinyin: 'yì'}, + { index: 163, sign: '耳', strokes: 6, translationKey: 'Ohr', pinyin: 'ěr', emoji:'👂'}, + { index: 164, sign: '臣', strokes: 6, translationKey: 'Diener; Minister', pinyin: 'yī'}, + { index: 165, sign: '𢦏', strokes: 6, translationKey: '', pinyin: ''}, + { index: 166, sign: '西', strokes: 6, translationKey: 'bedecken, Westen', compare: '193 yǒu 酉 Amphore', pinyin: 'xī'}, + { index: 167, sign: '朿', strokes: 6, translationKey: 'Dorn', compare: '192 shù 束 Bündel', pinyin: 'cì'}, + { index: 168, sign: '亚', strokes: 6, translationKey: 'unterlegen, zweitrangig, Asien', original: '亞', pinyin: 'yà'}, + { index: 169, sign: '而', strokes: 6, translationKey: 'Bart, und', pinyin: 'ér', emoji:'🧔‍'}, + { index: 170, sign: '页', strokes: 6, translationKey: 'Kopf, Seite', original: '頁', pinyin: 'yè'}, + { index: 171, sign: '至', strokes: 6, translationKey: 'erreichen, bis, nach', pinyin: 'zhì'}, + { index: 172, sign: '光', strokes: 6, translationKey: 'Glanz, Licht, Strahl', pinyin: 'guāng', emoji:'💡'}, + { index: 173, sign: '虍', strokes: 6, translationKey: 'Tiger', original: '虎', pinyin: 'hǔ', emoji:'🐅'}, + { index: 174, sign: '虫', strokes: 6, translationKey: 'Insekt, Wurm', pinyin: 'chóng', emoji:'🪱'}, + { index: 175, sign: '缶', strokes: 6, translationKey: 'Krug, Amphore', pinyin: 'fǒu', emoji:'🏺'}, + { index: 176, sign: '耒', strokes: 6, translationKey: 'Pflug, Deichsel', compare: '94 mù 木 Holz', pinyin: 'lěi'}, + { index: 177, sign: '舌', strokes: 6, translationKey: 'Zunge', pinyin: 'shé', emoji:'👅'}, + { index: 178, sign: '竹', strokes: 6, translationKey: 'Bambus', variant: '⺮', pinyin: 'zhú', emoji:'🎋'}, + { index: 179, sign: '臼', strokes: 6, translationKey: 'Mörser', pinyin: 'jiù'}, + { index: 180, sign: '自', strokes: 6, translationKey: 'kleine Nase, selbst', compare: '226 bí 鼻', pinyin: 'zì', emoji:'👃'}, + { index: 181, sign: '血', strokes: 6, translationKey: 'Blut', compare: 'vgl. 146 mǐn 皿 Schüssel', pinyin: 'xuè', emoji:'🩸'}, + { index: 182, sign: '舟', strokes: 6, translationKey: 'Boot, Kahn', pinyin: 'zhōu', emoji:'⛴️'}, + { index: 183, sign: '羽', strokes: 6, translationKey: 'Flügel', pinyin: 'yǔ'}, + { index: 184, sign: '艮', strokes: 6, translationKey: 'stur, aufrichtig', pinyin: 'gèn'}, + { index: 185, sign: '言', strokes: 7, translationKey: 'Wort, sprechen', short: '10 讠', pinyin: 'yán', emoji:'💬'}, + { index: 186, sign: '辛', strokes: 7, translationKey: 'bitter, scharf, mühsam', tooltip: 'TOOLTIP.N_TH_HEAVEN:n=8', pinyin: 'xīn', emoji: '☁️'}, + { index: 187, sign: '辰', strokes: 7, translationKey: 'Morgendämmerung', tooltip: 'TOOLTIP.N_TH_EARTH:n=5', pinyin: 'chén', emoji:'🌅'}, + { index: 188, sign: '麦', strokes: 7, translationKey: 'Weizen, Getreide', pinyin: 'mài', emoji:'🌾'}, + { index: 189, sign: '走', strokes: 7, translationKey: 'gehen', short: '47 辶', compare:'196 zú 足 Fuß', pinyin: 'zǒu'}, + { index: 190, sign: '赤', strokes: 7, translationKey: 'zinnoberrot, rot, treu, nackt', pinyin: 'chì'}, + { index: 191, sign: '豆', strokes: 7, translationKey: 'Bohne', pinyin: 'dòu', emoji:'🫘'}, + { index: 192, sign: '束', strokes: 7, translationKey: 'Bündel, schnüren', compare: '167 cì 朿 Dorn', pinyin: 'shù'}, + { index: 193, sign: '酉', strokes: 7, translationKey: 'Amphore', compare:'166 xī 西 Westen', tooltip:'TOOLTIP.N_TH_EARTH:n=9', pinyin: 'yǒu', emoji:'🏺'}, + { index: 194, sign: '豕', strokes: 7, translationKey: 'Schwein', original: '豬', pinyin: 'shǐ', emoji:'🐖'}, + { index: 195, sign: '里', strokes: 7, translationKey: 'Dorf, Meile, innen', pinyin: 'lǐ'}, + { index: 196, sign: '足', strokes: 7, translationKey: 'Fuß', pinyin: 'zú', emoji:'🦶'}, + { index: 197, sign: '采', strokes: 7, translationKey: 'pflücken, abnehmen', pinyin: 'cǎi'}, + { index: 198, sign: '豸', strokes: 7, translationKey: 'Schlange, Wurm', pinyin: 'zhì', emoji:'🐍'}, + { index: 199, sign: '谷', strokes: 7, translationKey: 'Tal, Schlucht', pinyin: 'gǔ'}, + { index: 200, sign: '身', strokes: 7, translationKey: 'Rumpf, Körper', pinyin: 'shēn'}, + { index: 201, sign: '角', strokes: 7, translationKey: 'Horn', pinyin: 'jiǎo'}, + { index: 202, sign: '青', strokes: 8, translationKey: 'blaugrün', pinyin: 'qīng'}, + { index: 203, sign: '龺', strokes: 8, translationKey: '', pinyin: ''}, + { index: 204, sign: '雨', strokes: 8, translationKey: 'Regen', pinyin: 'yǔ', emoji:'🌧️'}, + { index: 205, sign: '非', strokes: 8, translationKey: 'falsch, Fehler, Unrecht, nicht', pinyin: 'fēi'}, + { index: 206, sign: '齿', strokes: 8, translationKey: 'Backenzahn', compare: '99 yá 牙 Eckzahn', original:'齒', pinyin: 'fēi', emoji:'🦷'}, + { index: 207, sign: '黾', strokes: 8, translationKey: 'Kröte, Schildkröte', original: '黽', pinyin: 'mǐn', emoji:'🐢'}, + { index: 208, sign: '隹', strokes: 8, translationKey: 'kleiner Vogel', pinyin: 'zhuī', emoji:'🐣'}, + { index: 209, sign: '金', strokes: 8, translationKey: 'Gold, Metall', short: '147 jīn 钅', tooltip:'TOOLTIP.N_TH_ELEMENT:n=4', pinyin: 'jīn'}, + { index: 210, sign: '鱼', strokes: 8, translationKey: 'Fisch', original: '魚', pinyin: 'yú', emoji:'🐟'}, + { index: 211, sign: '音', strokes: 9, translationKey: 'Ton, Laut', pinyin: 'yīn', emoji:'🔊'}, + { index: 212, sign: '革', strokes: 9, translationKey: 'Leder, verändern', pinyin: 'gé'}, + { index: 213, sign: '是', strokes: 9, translationKey: 'sein, richtig, korrekt', pinyin: 'shì'}, + { index: 214, sign: '骨', strokes: 9, translationKey: 'Knochen', pinyin: 'gǔ', emoji:'🦴'}, + { index: 215, sign: '香', strokes: 9, translationKey: 'Duft, wohlriechend', compare: '149 hé 禾 Getreide', pinyin: 'xiāng'}, + { index: 216, sign: '鬼', strokes: 9, translationKey: 'Gespenst, Geist', pinyin: 'guǐ', emoji:'👻'}, + { index: 217, sign: '食', strokes: 9, translationKey: 'Nahrung, Speise', short: '62 彳 & 68 飠', pinyin: 'shí', emoji:'🥘'}, + { index: 218, sign: '高', strokes: 10, translationKey: 'hoch', variant: '髙', pinyin: 'gāo'}, + { index: 219, sign: '鬲', strokes: 10, translationKey: 'Kessel, Dreifuß', pinyin: 'lì'}, + { index: 220, sign: '髟', strokes: 10, translationKey: 'Haar', compare: '63 彡 Haarsträhne', pinyin: 'biāo', emoji:'💇‍'}, + { index: 221, sign: '麻', strokes: 11, translationKey: 'Hanf', pinyin: 'má', emoji:'🌿'}, + { index: 222, sign: '鹿', strokes: 11, translationKey: 'Hirsch', pinyin: 'lù', emoji:'🦌'}, + { index: 223, sign: '黑', strokes: 12, translationKey: 'schwarz', variant: '黒', pinyin: 'hēi', emoji:'🏴'}, + { index: 224, sign: '鼓', strokes: 13, translationKey: 'Trommel', pinyin: 'gǔ', emoji:'🥁'}, + { index: 225, sign: '鼠', strokes: 13, translationKey: 'Maus, Ratte', pinyin: 'shǔ', emoji:'🐁'}, + { index: 226, sign: '鼻', strokes: 13, translationKey: 'Nase', pinyin: 'bí', emoji:'👃'}, + // unclassifable signs: + { index: 227, sign: '〇', strokes: 1, translationKey: 'Null', original: '零', pinyin: 'líng'}, + { index: 228, sign: '乡', strokes: 3, translationKey: 'Heimat', pinyin: 'xiāng'}, + { index: 229, sign: '判', strokes: 7, translationKey: 'entscheiden', pinyin: 'pàn'}, + { index: 230, sign: '卵', strokes: 7, translationKey: 'Laich, Ei', pinyin: 'luǎn', emoji:'🥚'}, + { index: 231, sign: '凸', strokes: 5, translationKey: 'konvex', pinyin: 'tū'}, + { index: 232, sign: '甲', strokes: 5, translationKey: '', tooltip: 'TOOLTIP.N_TH_HEAVEN:n=1', pinyin: 'jiǎ', emoji: '☁️'}, + { index: 233, sign: '民', strokes: 4, translationKey: 'Volk', pinyin: 'mín'}, +]; diff --git a/src/app/data/words.ts b/src/app/data/words.ts new file mode 100644 index 0000000..3bad42b --- /dev/null +++ b/src/app/data/words.ts @@ -0,0 +1,104 @@ +import {Word, WORD_TYPE} from "../interfaces/word.data"; + +const FRUITS: Word[] = [ + { translationKey: "FRUITS.GRAPES", hanzi: '葡萄', pinyin: 'pútáo', composition: [['艹', '勹', '甫'], ['艹', '勹', '缶']], emoji: '🍇', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.ORANGE", hanzi: '橙子', pinyin: 'chéngzi', composition: [['木', '癶', '豆'], ['子']], emoji: '🍊', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.LEMON", hanzi: '柠檬', pinyin: 'níngméng', composition: [['木', '宀', '一', '丿'],['木', '艹', '冖', '一', '豕']], emoji: '🍋', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.APPLE", hanzi: '苹果', pinyin: 'píngguǒ', composition: [['艹', 'x'], ['果']], emoji: '🍎', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.PEAR", hanzi: '梨', pinyin: 'lí', composition: [['禾', '刂', '木']], emoji: '🍐', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.BANANA", hanzi: '香蕉', pinyin: 'xiāngjiāo', composition: [['香'],['艹','隹', '灬']], emoji: '🍌', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.KIWI", hanzi: '猕猴桃', pinyin: 'míhóutáo', composition: [['犭', '弓', '丶', '乛', '小'],['犭', '亻', '一', '亠', '矢'],['桃']], emoji: '🥝', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.CHERRY", hanzi: '樱桃', pinyin: 'yīngtáo', composition: [['木', '贝', '贝', '女'],['桃']], emoji: '🍒', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.RASPBERRY", hanzi: '莓', pinyin: 'méi', composition: [['莓']], type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.STRAWBERRY", hanzi: '草莓', pinyin: 'cǎoméi', composition: [['艹','日','十'],['莓']], emoji: '🍓', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.BLUEBERRY", hanzi: '蓝莓', pinyin: 'lánméi', composition: [['艹', '刂', '𠂉', '丶', '皿'], ['莓']], emoji: '🫐', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.COCONUT", hanzi: '椰子', pinyin: 'yēzi', composition: [['木', '耳', '⻏'], ['子']], emoji: '🥥', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.PINEAPPLE", hanzi: '菠萝', pinyin: 'bōluó', composition: [['艹', '氵', '皮'],['艹', '罒', '夕']], emoji: '🍍', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.MANGO", hanzi: '芒果', pinyin: 'mángguǒ', composition: [['艹', '亡'],['果']], emoji: '🥭', type: [WORD_TYPE.NOUN]}, + { translationKey: "FRUITS.WATERMELON", hanzi: '西瓜', pinyin: 'xīguā', composition: [['西'],['瓜']], emoji: '🍉', type: [WORD_TYPE.NOUN]} +]; + +const FOOD: Word[] = [ + { translationKey: "eat", hanzi: '吃', pinyin: 'chī', composition: [['口', '𠂉', '乙']], type: [WORD_TYPE.VERB]}, + { translationKey: "dish, cuisine", hanzi: '菜', pinyin: 'cài', composition: [['艹', '爪', '木']], type: [WORD_TYPE.NOUN]}, +]; + +const DRINKS: Word[] = [ + { translationKey: "DRINKS.DRINK", hanzi: '喝', pinyin: 'hē', composition: [['口', '日', '丶', '𠃌', '人', '乙']], type: [WORD_TYPE.VERB]}, + { translationKey: "DRINKS.TEA", hanzi: '茶', pinyin: 'chá', composition: [['艹', '人', '木']], type: [WORD_TYPE.NOUN]}, + { translationKey: "DRINKS.COFFEE", hanzi: '咖啡', pinyin: 'kāfēi', composition: [['口', '力', '口'], ['口','非']], emoji: '☕', type: [WORD_TYPE.NOUN]}, + { translationKey: "DRINKS.BEER", hanzi: '啤酒', pinyin: 'píjiǔ', composition: [['口', '申', '十'], ['氵','酉']], emoji: '🍻', type: [WORD_TYPE.NOUN]} +]; + +const GET_TO_KNOW: Word[] = [ + { translationKey: "to be called", hanzi: '叫', pinyin: 'jiào', composition: [['口', '丩']], type: [WORD_TYPE.VERB]}, + { translationKey: "surname, one’s family name is", hanzi: '姓', pinyin: 'xìng', composition: [['女', '生']], type: [WORD_TYPE.VERB, WORD_TYPE.NOUN]}, + { translationKey: "name", hanzi: '名字', pinyin: 'míngzi', composition: [['夕', '口'], ['宀', '子']], type: [WORD_TYPE.NOUN]}, + { translationKey: "a person of which country", hanzi: '哪国人', pinyin: 'nǎguórén', composition: [['口', '月', '⻏'], ['国'], ['人']], type: [WORD_TYPE.QUESTION_WORD]}, + { translationKey: "get to know, meet", hanzi: '认识', pinyin: 'rènshì', composition: [['讠', '人'], ['讠', '口', '丶', '丶']], type: [WORD_TYPE.VERB]}, + { translationKey: "very", hanzi: '很', pinyin: 'hěn', composition: [['彳', '目', '匕']], type: [WORD_TYPE.ADVERB]}, + { translationKey: "happy", hanzi: '高兴', pinyin: 'gāoxìng', composition: [['高'], ['⺌','一','丶','丶']], type: [WORD_TYPE.ADJECTIVE]}, + { translationKey: "Goodbye", hanzi: '再见', pinyin: 'zàijiàn', composition: [['一', '土', '月'], ['见']], type: [WORD_TYPE.PHRASE]}, + { translationKey: "meassure word", hanzi: '个', pinyin: 'gè', composition: [['人', '丨']], type: [WORD_TYPE.MEASSURE_WORD]}, + { translationKey: "and", hanzi: '和', pinyin: 'hé', composition: [['禾', '口']], type: [WORD_TYPE.CONJUNCTION]}, + { translationKey: "introduce", hanzi: '介绍', pinyin: 'jièshào', composition: [['人', '八'], ['纟', '刀', '口']], type: [WORD_TYPE.VERB]}, +]; + +const VERBS: Word[] = [ + { translationKey: "be", hanzi: '是', pinyin: 'shì', composition: [['是']], type: [WORD_TYPE.VERB]}, + { translationKey: "want", hanzi: '要', pinyin: 'yào', composition: [['一', '罒', '女']], type: [WORD_TYPE.VERB]}, + { translationKey: "like", hanzi: '喜欢', pinyin: 'xǐhuan', composition: [['土', '口', '丶', '丶', '一', '口'], ['又', '欠']], type: [WORD_TYPE.VERB]}, +] + + +const ADJECTIVES: Word[] = [ + { translationKey: "busy", hanzi: '忙', pinyin: 'máng', composition: [['忄', '亡']], type: [WORD_TYPE.ADJECTIVE]}, + { translationKey: "tired, tiring", hanzi: '累', pinyin: 'lèi', composition: [['田', '幺', '小']], type: [WORD_TYPE.ADJECTIVE]}, + { translationKey: "good looking", hanzi: '好看', pinyin: 'hǎokàn', composition: [['好'], ['手', '目']], type: [WORD_TYPE.ADJECTIVE]}, + { translationKey: "beautiful", hanzi: '漂亮', pinyin: 'piàoliàng', composition: [['氵', '一', '罒', '一', '一', '小'], ['亠', '口', '冖', '几']], type: [WORD_TYPE.ADJECTIVE]}, + { translationKey: "cute, lovely", hanzi: '可爱', pinyin: 'kěài', composition: [['𠃌', '口'], ['爫', '冖', '𠂇', '又']], type: [WORD_TYPE.ADJECTIVE]}, +]; + + +const COUNTRIES: Word[] = [ + { translationKey: "US", hanzi: '美国', pinyin: 'měiguó', composition: [['羊', '大'], ['国']], type: [WORD_TYPE.NOUN]}, + { translationKey: "China", hanzi: '中国', pinyin: 'zhōngguó', composition: [['中'], ['国']], type: [WORD_TYPE.NOUN]}, + { translationKey: "Japan", hanzi: '日本', pinyin: 'rìběn', composition: [['日'], ['木', '一']], type: [WORD_TYPE.NOUN]}, +] + +const PERSONS: Word[] = [ + { translationKey: "student", hanzi: '学生', pinyin: 'xuéshēng', composition: [['⺌', '冖', '子'], ['生']], type: [WORD_TYPE.NOUN]}, + { translationKey: "teacher", hanzi: '老师', pinyin: 'lǎoshī', composition: [['耂'], ['刂', '一', '巾']], type: [WORD_TYPE.NOUN]}, + { translationKey: "measure word for family members", hanzi: '口', pinyin: 'kǒu', composition: [['口']], type: [WORD_TYPE.MEASSURE_WORD]}, + { translationKey: "father", hanzi: '爸爸', pinyin: 'bàba', composition: [['丶', '丶', '乂', '己', '丨', '丨']], type: [WORD_TYPE.NOUN]}, + { translationKey: "mother", hanzi: '妈妈', pinyin: 'māma', composition: [['女', '马']], type: [WORD_TYPE.NOUN]}, + { translationKey: "siblings", hanzi: '兄弟姐妹', pinyin: 'xiōngdì jiěmèi', composition: [['口', '儿'], [], [], []], type: [WORD_TYPE.NOUN]}, + { translationKey: "older brother", hanzi: '哥哥', pinyin: 'gēge', composition: [['一', '丿', '口', '一', '口']], type: [WORD_TYPE.NOUN]}, + { translationKey: "older sister", hanzi: '姐姐', pinyin: 'jiějie', composition: [['女', '目']], type: [WORD_TYPE.NOUN]}, + { translationKey: "younger brother", hanzi: '弟弟', pinyin: 'dìdi', composition: [['丶', '丶', '弓', '丿', '丨']], type: [WORD_TYPE.NOUN]}, + { translationKey: "younger sister", hanzi: '妹妹', pinyin: 'mèimei', composition: [['女', '十', '木']], type: [WORD_TYPE.NOUN]}, + { translationKey: "family member", hanzi: '家人', pinyin: 'jiārén', composition: [['家'], ['人']], type: [WORD_TYPE.NOUN]}, +]; + +const QUESTION_WORDS: Word[] = [ + { translationKey: "QUESTION_WORDS.WHO", hanzi: '谁', pinyin: 'shéi', composition: [['x']], type: [WORD_TYPE.QUESTION_WORD]}, +]; +const PERSONAL_PRONOUN: Word[] = [ + { translationKey: "PERSONAL_PRONOUNS.I", hanzi: '我', pinyin: 'wǒ', composition: [['丿', '一', '一', '戈']], type: [WORD_TYPE.PRONOUN]}, + { translationKey: "PERSONAL_PRONOUNS.HE", hanzi: '他', pinyin: 'nǐ', composition: [['亻', 'x']], type: [WORD_TYPE.PRONOUN]}, + { translationKey: "PERSONAL_PRONOUNS.YOU_PL", hanzi: '你们', pinyin: 'nǐmen', composition: [['你'], ['亻', '门']], type: [WORD_TYPE.PRONOUN]}, +]; + +export const WORDS: Word[] = [ + ...FRUITS, + ...FOOD, + ...DRINKS, + ...GET_TO_KNOW, + ...VERBS, + ...ADJECTIVES, + ...COUNTRIES, + ...PERSONS, + ...QUESTION_WORDS, + ...PERSONAL_PRONOUN, +]; + diff --git a/src/app/interfaces/character.data.ts b/src/app/interfaces/character.data.ts new file mode 100644 index 0000000..b662bd6 --- /dev/null +++ b/src/app/interfaces/character.data.ts @@ -0,0 +1,9 @@ +export interface Character { + hanzi: string, + pinyin: string, + translationKey: string, + composition: string[], + + usedIn: string[], + emoji?: string; +} diff --git a/src/app/interfaces/radical.data.ts b/src/app/interfaces/radical.data.ts new file mode 100644 index 0000000..f619fc3 --- /dev/null +++ b/src/app/interfaces/radical.data.ts @@ -0,0 +1,17 @@ +export interface Radical { + index: number, + sign: string, + translationKey: string, + pinyin: string, + + tooltip?: string, + compare?: string, + original?: string, + short?: string, + long?: string, + variant?: string, + + emoji?: string, + strokes: number, + +} diff --git a/src/app/interfaces/word.data.ts b/src/app/interfaces/word.data.ts new file mode 100644 index 0000000..9b0fdd6 --- /dev/null +++ b/src/app/interfaces/word.data.ts @@ -0,0 +1,13 @@ +export interface Word { + translationKey: string; + hanzi: string; + pinyin: string; + composition: string[][]; + emoji?: string; + + type: WORD_TYPE[]; +} + +export enum WORD_TYPE { + VERB, NOUN, MEASSURE_WORD, PRONOUN, QUESTION_WORD, ADJECTIVE, ADVERB, PHRASE, CONJUNCTION, COMB_EL +} diff --git a/src/app/pages/aggregated-page/aggregated-page.component.css b/src/app/pages/aggregated-page/aggregated-page.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/aggregated-page/aggregated-page.component.html b/src/app/pages/aggregated-page/aggregated-page.component.html new file mode 100644 index 0000000..e047203 --- /dev/null +++ b/src/app/pages/aggregated-page/aggregated-page.component.html @@ -0,0 +1,3 @@ + + + diff --git a/src/app/pages/aggregated-page/aggregated-page.component.spec.ts b/src/app/pages/aggregated-page/aggregated-page.component.spec.ts new file mode 100644 index 0000000..63e885a --- /dev/null +++ b/src/app/pages/aggregated-page/aggregated-page.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AggregatedPageComponent } from './aggregated-page.component'; + +describe('AggregatedPageComponent', () => { + let component: AggregatedPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AggregatedPageComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AggregatedPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/aggregated-page/aggregated-page.component.ts b/src/app/pages/aggregated-page/aggregated-page.component.ts new file mode 100644 index 0000000..0a55967 --- /dev/null +++ b/src/app/pages/aggregated-page/aggregated-page.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-aggregated-page', + templateUrl: './aggregated-page.component.html', + styleUrl: './aggregated-page.component.css' +}) +export class AggregatedPageComponent { + +} diff --git a/src/app/pages/calendar-page/calendar-page.component.css b/src/app/pages/calendar-page/calendar-page.component.css new file mode 100644 index 0000000..5f07401 --- /dev/null +++ b/src/app/pages/calendar-page/calendar-page.component.css @@ -0,0 +1,13 @@ +.calendar-background-image { + background-image: url("../../../assets/img/sky_earth.jpg"); + background-size:100% 100%; + width: 100vw; + min-height: 100vh; + height: auto; +} + +.frame { + background-color: rgba(1, 1, 1, .5); + color: white; + border: 0; +} diff --git a/src/app/pages/calendar-page/calendar-page.component.html b/src/app/pages/calendar-page/calendar-page.component.html new file mode 100644 index 0000000..a1f3ecb --- /dev/null +++ b/src/app/pages/calendar-page/calendar-page.component.html @@ -0,0 +1,23 @@ +
+

 

+
+

{{"CHINESE_CALENDAR.INTRODUCTION" | translate}}

+

{{"CHINESE_CALENDAR.HEAVEN.EXPLANATION" | translate}}

+

{{"CHINESE_CALENDAR.EARTH.EXPLANATION" | translate}}

+
+
+
+
{{"CHINESE_CALENDAR.HEAVEN.TITLE" | translate}}
+
    +
  1. {{sign.sign}} {{sign.pinyin}} element: {{sign.phase}}
  2. +
+
+ +
+
{{"CHINESE_CALENDAR.EARTH.TITLE" | translate}}
+
    +
  1. {{sign.sign}} {{sign.pinyin}} animal: {{sign.animal}}
  2. +
+
+
+
diff --git a/src/app/pages/calendar-page/calendar-page.component.spec.ts b/src/app/pages/calendar-page/calendar-page.component.spec.ts new file mode 100644 index 0000000..4672110 --- /dev/null +++ b/src/app/pages/calendar-page/calendar-page.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CalendarPageComponent } from './calendar-page.component'; + +describe('CalendarPageComponent', () => { + let component: CalendarPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CalendarPageComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CalendarPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/calendar-page/calendar-page.component.ts b/src/app/pages/calendar-page/calendar-page.component.ts new file mode 100644 index 0000000..694c300 --- /dev/null +++ b/src/app/pages/calendar-page/calendar-page.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import {EARTH_BRANCHES} from "../../data/earth.branches"; +import {HEAVENLY_STEMS} from "../../data/heaven.stems"; + +@Component({ + selector: 'app-calendar-page', + templateUrl: './calendar-page.component.html', + styleUrl: './calendar-page.component.css' +}) +export class CalendarPageComponent { + + public readonly earthSigns = EARTH_BRANCHES; + public readonly heavenSigns = HEAVENLY_STEMS; + +} diff --git a/src/app/pages/home-page/home-page.component.css b/src/app/pages/home-page/home-page.component.css new file mode 100644 index 0000000..3581429 --- /dev/null +++ b/src/app/pages/home-page/home-page.component.css @@ -0,0 +1,48 @@ +.home { + display: flex; + flex-direction: row; + justify-content: left; + height: 100vh; +} + +.left { + width: 480px; + z-index: 10; +} + +.right { + padding-top: 10vh; + height: 100vh; +} + +img { + position: absolute; + top: 0; + left: 0; + height: 100vh; +} + +.column { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.button-col { + display: flex; + flex-direction: column; + max-width: 350px; +} + +.column button { + margin: 10px; + justify-content: flex-start; +} + +app-topic-banner { + z-index: 1; +} + +.bottom { + align-self: flex-end; +} diff --git a/src/app/pages/home-page/home-page.component.html b/src/app/pages/home-page/home-page.component.html new file mode 100644 index 0000000..a406d29 --- /dev/null +++ b/src/app/pages/home-page/home-page.component.html @@ -0,0 +1,33 @@ +
+
+ + +
+
+

{{"HOME.TITLE" | translate }}

+

{{"HOME.INTRODUCTION" | translate }}

+

{{"HOME.SUBTITLE" | translate }}

+ +
+
+ + + +
+ + +
+
+
+ + +
+
+ + diff --git a/src/app/pages/home-page/home-page.component.spec.ts b/src/app/pages/home-page/home-page.component.spec.ts new file mode 100644 index 0000000..c6f955d --- /dev/null +++ b/src/app/pages/home-page/home-page.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomePageComponent } from './home-page.component'; + +describe('HomePageComponent', () => { + let component: HomePageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HomePageComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HomePageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/home-page/home-page.component.ts b/src/app/pages/home-page/home-page.component.ts new file mode 100644 index 0000000..874cc61 --- /dev/null +++ b/src/app/pages/home-page/home-page.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import {WORDS} from "../../data/words"; + +@Component({ + selector: 'app-home-page', + templateUrl: './home-page.component.html', + styleUrl: './home-page.component.css' +}) +export class HomePageComponent { + + public scrollToElement(id: string) { + let el = document.getElementById(id); + if (el) { + el.scrollIntoView({behavior: 'smooth'}); + } + } + + constructor() { + console.log("words:"+ WORDS.length); + } + +} diff --git a/src/app/services/character.service.spec.ts b/src/app/services/character.service.spec.ts new file mode 100644 index 0000000..bad44b7 --- /dev/null +++ b/src/app/services/character.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CharacterService } from './character.service'; + +describe('CharacterService', () => { + let service: CharacterService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CharacterService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/character.service.ts b/src/app/services/character.service.ts new file mode 100644 index 0000000..5ba2130 --- /dev/null +++ b/src/app/services/character.service.ts @@ -0,0 +1,133 @@ +import { Injectable } from '@angular/core'; +import {RADICALS} from "../data/radicals"; +import {COMBINATIONS} from "../data/combinations"; +import {WORDS} from "../data/words"; +import {Radical} from "../interfaces/radical.data"; +import {Word, WORD_TYPE} from "../interfaces/word.data"; + +@Injectable({ + providedIn: 'root' +}) +export class CharacterService { + + + private radicalMap = new Map(); + private characterMap = new Map(); + constructor() { + RADICALS.forEach(r => { + this.characterMap.set(r.sign, {c: r, usedIn: []}); + this.radicalMap.set(r.sign, r); + }); + let radicalN = this.characterMap.size; + + COMBINATIONS.forEach(c => { + this.characterMap.set(c.hanzi, {c, usedIn: []}) + this.updateUsedInForCompostion(c.composition[0], c.hanzi); + }) + let combinationsN = this.characterMap.size - radicalN; + + WORDS.forEach(w => { + w.composition.forEach((comp, i) => { + if (comp.length > 1 && w.hanzi.at(i)) { + let sign = w.hanzi.at(i)!; + if (this.characterMap.has(sign)) { + console.log(sign + ' character already exist in map, will be overwritten'); + } + let character: Word = {hanzi: sign, pinyin: w.pinyin, translationKey: '', composition: [comp], type: [WORD_TYPE.COMB_EL]}; + this.characterMap.set(sign, {c: character, usedIn: []}); + // TODO pinyin only syllable + this.updateUsedInForCompostion(comp, w.hanzi); + } else if (comp.length == 1 && w.hanzi.at(i) && comp[0] == w.hanzi.at(i)) { + this.updateUsedInForCombinationElement(comp[0], w.hanzi); + } + this.characterMap.set(w.hanzi, {c: w, usedIn: []}); + }); + }) + let wordCharactersN = this.characterMap.size - radicalN - combinationsN; + + + + console.log(radicalN + " Radicals"); + console.log(combinationsN + " Characters defined as Combination"); + console.log(wordCharactersN + " Characters defined in Words"); + } + + private updateUsedInForCompostion(composition: string[], sign: string) { + composition.forEach(c => { + if (this.characterMap.has(c)) { + let character = this.characterMap.get(c)!; + let usedIn = character.usedIn; + if (!usedIn.includes(sign)) { + usedIn.push(sign); + } + this.characterMap.set(c, {...character, usedIn}) + } else { + console.log('ERROR: '+ c + ' is not defined for sign: ' + sign); + // TODO: Schreibvarianten in Map einpflegen ?? + } + }); + } + + private updateUsedInForCombinationElement(combEl: string, word: string) { + if (this.characterMap.has(combEl)) { + let character = this.characterMap.get(combEl)!; + if ('composition' in character.c) { + (character.c).composition.forEach(comp => this.updateUsedInForCompostion(comp, word)); + } + } + } + + public getOccurencesOfSign(sign: string): Word[] { + if (this.characterMap.has(sign)) { + let character = this.characterMap.get(sign)!; + let result: Word[] = []; + character.usedIn.forEach(c => { + if (this.characterMap.has(c)) { + let word = this.characterMap.get(c)!.c; + result.push(word); + } + }); + return result; + } else { + return []; + } + } + + public isRadical(sign: string): boolean { + return this.radicalMap.has(sign); + } + + public getRadical(sign: string): Radical | undefined { + return this.radicalMap.get(sign); + } + + public getRandomRadical(): Radical { + let randomIndex = Math.floor(Math.random() * RADICALS.length); + return RADICALS.at(randomIndex)!; + } + + public getWord(comb: string): Word | undefined { + if (this.characterMap.has(comb)) { + let word = this.characterMap.get(comb)!; + if ('composition' in word.c) { + return word.c; + } + } + return undefined; + } + + public getComposition(comb: string): string[] { + if (comb.length > 1) { + return []; + } + + if (this.characterMap.has(comb)) { + let word = this.characterMap.get(comb)!; + if ('composition' in word.c) { + return (word.c).composition[0]; + } + } + return []; + } + +} diff --git a/src/assets/i18n/ch.json b/src/assets/i18n/ch.json new file mode 100644 index 0000000..8970680 --- /dev/null +++ b/src/assets/i18n/ch.json @@ -0,0 +1,6 @@ +{ + "FOOD": { + "TOPIC_TITLE": "Food", + "COFFEE": "咖啡" + } +} diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json new file mode 100644 index 0000000..5cd3737 --- /dev/null +++ b/src/assets/i18n/de.json @@ -0,0 +1,12 @@ +{ + "TOOLTIP": { + "COMPARE": "vgl. ", + "SHORT_FORM": "Kurzform von", + "LONG_FORM": "Langform von", + "ORIGINAL": "Langzeichen", + "VARIANT": "Schreibvariante", + "N_TH_HEAVEN": "{{n}}. Himmelsstamm", + "N_TH_EARTH": "{{n}}. Erdzweig", + "N_TH_ELEMENT": "{{n}}. Element" + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json new file mode 100644 index 0000000..6fc3589 --- /dev/null +++ b/src/assets/i18n/en.json @@ -0,0 +1,143 @@ +{ + "HOME": { + "TITLE": "Welcome! 欢迎 (Huānyíng)", + "INTRODUCTION": "to the enchanting realm of Chinese characters.", + "SUBTITLE": "Embrace the challenge and beauty of learning Chinese and explore its creativity" + }, + "TOOLBAR.TITLE": "Chinese Puzzle", + "LANGUAGE": { + "EN": "English", + "DE": "German" + }, + "PINYIN": { + "TITLE": "Pinyin (拼音)", + "PINYIN_INTRODUCTION": "is the Romanization system for transcribing Mandarin sounds into Latin script. It includes tone markings to indicate the correct pronunciation.", + "TONE_INTRODUCTION": "Chinese is a tonal language, meaning that the pitch or intonation used when pronouncing a word can change its meaning. There are four main tones:", + "TONE_1": "First tone: → High and level (e.g., mā 妈 - mother)", + "TONE_2": "Second tone: ↗︎ Rising (e.g., má 麻 - hemp)", + "TONE_3": "Third tone: ↘︎↗︎ Falling-rising (e.g., mǎ 马 - horse)", + "TONE_4": "Fourth tone: ↘︎ Falling (e.g., mà 骂 - scold)", + "DOUBLE_VOCALS": { + "EXPLANATION": "The placement of tones in syllables with multiple vowels follows a specific set of ordered rules:", + "RULE_1": "If there is an a-vowel present: The tone is placed on that a-vowel (e.g., hǎo 好 good; xiǎo 小 little)", + "RULE_2": "If there is no a-vowel present: The tone is placed on the o- or e-vowel (e.g., kǒu 口 mounth; huǒ 火 fire; yuè 月 moon/month) - Note: Combinations like \"oe\" or \"eo\" do not occur.", + "RULE_3": " In cases where the above rules do not apply (absent a/o/e), the tone is placed on the last vowel (e.g., shuǐ 水 water)" + }, + "CHANGING_INTONATION": { + "EXPLANATION": "When multiple syllables with the third tone (falling-rising ↘︎↗︎) appear consecutively, the intonation of some syllables changes to the first tone (︎rising ↗︎):", + "2_TONES": "Two syllables ↘︎↗︎ ↘︎↗︎ become ︎↗︎︎ ↘︎↗︎ (e.g., nǐ hǎo 你好)", + "3_TONES": "Three syllables ↘︎↗︎ ↘︎↗︎ ↘︎↗︎ become ↘︎↗︎ ↗︎ ↘︎↗︎ (e.g., wǒ hěn hǎo 我很好)", + "4_TONES": "Four syllables ↘︎↗︎ ↘︎↗︎ ↘︎↗︎ ↘︎↗︎ become ↗︎ ↘︎↗︎ ↗︎ ↘︎↗︎ ", + "BU": "Additionally, when the negation word bù (不) precedes a syllable with a falling tone (↘︎), it changes its intonation to a rising tone (↗︎), pronounced bú. For example:", + "BU_EXAMPLE_1": "\"wǒ bù è le\" (I am not hungry.) changes to \"wǒ bú è le\"", + "BU_EXAMPLE_2": "\"wǒ bù lèi le\" (I am not tired.) to \"wǒ bú lèi le\"" + } + }, + "RADICAL_OVERVIEW": { + "TITLE": "Radicals (bùshǒu, 部首)", + "INTRO": "Chinese uses logographic characters known as hànzì (汉字). Each character represents a syllable and often corresponds to a word or part of a word. There are thousands of characters. Fortunately, they are based on a limited set of root elements, known as radicals (bùshǒu, 部首). Like a puzzle, radicals can be combined to a character. They often provide clues to the characters meanings or pronunciations.", + "OVERVIEW": "Browse through this overview of radicals to form a basic understanding. Select some to dive deeper into more complex characters, composed of these radicals.", + "HINT": "This display features simplified characters (jiǎntǐzì 简体字), which have been the official standard in China since 1950. By hovering over these characters, you can view the original traditional characters (fántǐzì 繁体字) or other writing variants, when available.", + "HINT2": "Some radicals are related to each other and appear in a long form and in a shorter form.", + "EXAMPLE_1.TITLE": "Water (125 shuǐ 水)", + "EXAMPLE_1.SUB": "This radical is associated with water-related concepts and appears in its shorter version (40 氵) in characters like 河 (hé, river) and 海 (hǎi, sea).", + "EXAMPLE_2.TITLE": "Person (23 rén 人)", + "EXAMPLE_2.SUB": "This radical indicates human-related concepts and appears in its shorter version (21 亻) in characters like 你 (nǐ, you) and 他 (tā, he).", + "EXAMPLE_3": "Tree (94 mù 木): This radical relates to trees and wood, found in characters like 林 (lín, forest) and 森 (sēn, woods).", + "SHOW_MORE": "Show more", + "SHOW_LESS": "Show less", + "OCCURENCES": "Occurences" + }, + "VIEW": { + "GRID": "Grid view", + "SINGLE": "Single view" + }, + "BUTTON": { + "GOTO_PINYIN_TONE": "Learn more about pinyin", + "GOTO_RADICALS": "Get started with the root elements" + }, + "TOOLTIP": { + "COMPARE": "compare ", + "SHORT_FORM": "short form of", + "LONG_FORM": "long form of", + "ORIGINAL": "original sign", + "VARIANT": "writing variant", + "N_TH_HEAVEN": "{{n}}. Heavenly Stem", + "N_TH_EARTH": "{{n}}. Earthly Branch", + "N_TH_ELEMENT": "{{n}}. Element" + }, + "SNACKBAR": { + "FIND_WORDS.MESSAGE": "Find chinese words composed of: {{radicals}}", + "FIND_WORDS.ACTION": "GO" + }, + "SIDECAR": { + "TITLE": "Characters", + "CHARACTERS_FOUND": "{{amount}} characters were found for the radical: {{radicals}}" + }, + "CHINESE_CALENDAR": { + "INTRODUCTION": "The traditional Chinese calendar is based on a 60-year cycle that combines the 10 heavenly stems and the 12 earthly branches.", + "HEAVEN.TITLE": "Heavenly Stems", + "HEAVEN.EXPLANATION": "The heavenly stems arise from the interplay of Yīn and Yáng ☯ with the five elements (mù 木 wood, huǒ 火 fire, tǔ 土 earth, jīn 金 metal and shuǐ 水 water), resulting in a total of 10 stems. Today, these stems serve as enumeration systems in various fields, including medicine, education, and sports leagues.", + "EARTH.TITLE": "Earthly Branches", + "EARTH.EXPLANATION": "The earthly branches are used to enumerate the animal signs in the Chinese zodiac, to represent the (double) hours of the day, and to mark the seasons of the year. They also serve as navigational references for seafarers and astronomers." + }, + "PERSONAL_PRONOUNS": { + "TOPIC_TITLE": "Personal pronouns", + "I": "I", + "YOU": "you", + "HE": "he", + "SHE": "she", + "IT": "it", + "WE": "we", + "YOU_PL": "you", + "THEY": "they" + }, + "QUESTION_WORDS": { + "TOPIC_TITLE": "Question words", + "WHAT": "What", + "WHICH": "Which", + "WHEN": "When", + "How": "How", + "WHY": "Why", + "BECAUSE": "because", + "QUESTION_PARTICLE": "Question particle for yes/no questions", + "WHERE": "Where", + "WHO": "Who", + "WHOM": "Whom" + }, + "WEATHER": { + "TOPIC_TITLE": "Weather" + }, + "NATURE": { + "TOPIC_TITLE": "Nature" + }, + "FAMILY": { + "TOPIC_TITLE": "Family" + }, + "DRINKS": { + "TOPIC_TITLE": "Drinks", + "COFFEE": "Coffee", + "BEER": "Beer" + }, + "FRUITS": { + "TOPIC_TITLE": "Fruits", + "FRUIT": "Fruit", + "GRAPES": "Grapes", + "ORANGE": "Orange", + "LEMON": "Lemon", + "APPLE": "Apple", + "PEAR": "Pear", + "BANANA": "Banana", + "PEACH": "Peach", + "KIWI": "Kiwi", + "CHERRY": "Cherry", + "RASPBERRY": "Raspberry", + "STRAWBERRY": "Strawberry", + "BLUEBERRY": "Blueberry", + "COCONUT": "Coconut", + "PINEAPPLE": "Pineapple", + "MANGO": "Mango", + "WATERMELON": "Watermelon" + } +} + diff --git a/src/assets/i18n/pinyin.json b/src/assets/i18n/pinyin.json new file mode 100644 index 0000000..a84cde3 --- /dev/null +++ b/src/assets/i18n/pinyin.json @@ -0,0 +1,5 @@ +{ + "FOOD": { + "COFFEE": "kā-fēi" + } +} diff --git a/src/assets/img/20240913_123720.jpg b/src/assets/img/20240913_123720.jpg new file mode 100644 index 0000000..99678fa Binary files /dev/null and b/src/assets/img/20240913_123720.jpg differ diff --git a/src/assets/img/home_dark.jpg b/src/assets/img/home_dark.jpg new file mode 100644 index 0000000..f27cfcd Binary files /dev/null and b/src/assets/img/home_dark.jpg differ diff --git a/src/assets/img/home_light.jpg b/src/assets/img/home_light.jpg new file mode 100644 index 0000000..98dba76 Binary files /dev/null and b/src/assets/img/home_light.jpg differ diff --git a/src/assets/img/sky_earth.jpg b/src/assets/img/sky_earth.jpg new file mode 100644 index 0000000..5d129e8 Binary files /dev/null and b/src/assets/img/sky_earth.jpg differ diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000..f8810a4 Binary files /dev/null and b/src/favicon.ico differ diff --git a/src/index.html b/src/index.html index d5b96de..e7ecc5a 100644 --- a/src/index.html +++ b/src/index.html @@ -2,12 +2,15 @@ - Caligraphy - + Chinese Puzzle + + + + - + diff --git a/src/main.ts b/src/main.ts index 35b00f3..421d764 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,5 @@ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { AppComponent } from './app/app.component'; +import {AppModule} from './app/app.module'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -bootstrapApplication(AppComponent, appConfig) - .catch((err) => console.error(err)); +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/src/styles.css b/src/styles.css index 90d4ee0..ccbfd5f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,56 @@ -/* You can add global styles to this file, and also import other style files */ +html, body { + height: 100%; + width: 100%; +} + +body { + font-family: Roboto, "Helvetica Neue", sans-serif; + margin: 0; +} + +.darkMode { + background-color: #161215; + color: white; +} + +.lightMode { + background-color: white; + color: #161215; +} + +.darkMode .light-img { + visibility: hidden; +} + +.darkMode .dark-img { + visibility: visible; +} + +.lightMode .dark-img { + visibility: hidden; +} + +.lightMode .light-img { + visibility: visible; +} + +.frame { + margin: 25px; + padding: 25px; + border-radius: 25px; + border: dimgrey 1px solid; +} + +.row { + display: flex; + flex-direction: row; +} + +.lightMode .mat-button-toggle-appearance-standard { + color: dimgrey; +} + +.lightMode .mat-button-toggle-appearance-standard.mat-button-toggle-checked, +.lightMode .carousel .mat-mdc-icon-button { + background-color: #42008a; +}