commit 36f1b746bddeae5ab9f935e25fd8b15708731f68 Author: e2e Date: Sat Jul 11 11:03:59 2026 +0200 chore: heygermany customer project diff --git a/.github/actions/detect-changes/action.yml b/.github/actions/detect-changes/action.yml new file mode 100644 index 0000000..072e206 --- /dev/null +++ b/.github/actions/detect-changes/action.yml @@ -0,0 +1,56 @@ +name: Detect Changes +description: Detect changed files and handle [deploy-all] commit override + +outputs: + api: + description: Whether API changed + value: ${{ steps.override.outputs.api }} + dbmigrate: + description: Whether DBMigrate changed + value: ${{ steps.override.outputs.dbmigrate }} + website: + description: Whether WebSite changed + value: ${{ steps.override.outputs.website }} + +runs: + using: "composite" + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: generated-code + path: ${{ github.workspace }} + + - id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + api: + - 'backends/express/**' + - 'sql/**' + - 'packages/**' + website: + - 'apps/website/**' + - 'packages/**' + + - id: override + shell: bash + run: | + COMMIT_MESSAGE="${{ github.event.head_commit.message }}" + FORCE_PUSH="${{ github.event.forced }}" + + echo "Commit message: $COMMIT_MESSAGE" + echo "Forced push: $FORCE_PUSH" + + # Temporarily always return true for all services + echo "api=true" >> $GITHUB_OUTPUT + echo "website=true" >> $GITHUB_OUTPUT + + # if [ "$FORCE_PUSH" = "true" ] || echo "$COMMIT_MESSAGE" | grep -q "\[deploy-all\]"; then + # echo "api=true" >> $GITHUB_OUTPUT + # echo "website=true" >> $GITHUB_OUTPUT + # else + # echo "api=${{ steps.filter.outputs.api }}" >> $GITHUB_OUTPUT + # echo "website=${{ steps.filter.outputs.website }}" >> $GITHUB_OUTPUT + # fi diff --git a/.github/actions/ecr-build-and-push/action.yml b/.github/actions/ecr-build-and-push/action.yml new file mode 100644 index 0000000..69b0275 --- /dev/null +++ b/.github/actions/ecr-build-and-push/action.yml @@ -0,0 +1,67 @@ +name: Build and Push to ECR +description: Fully prepares, builds, and pushes a Docker image to AWS ECR. + +inputs: + dockerfile: + description: 'Path to Dockerfile' + required: true + image-name: + description: 'ECR repo name (e.g. api or dbmigrate)' + required: true + aws-region: + description: 'AWS region' + required: true + aws-access-key-id: + description: 'AWS Access Key ID' + required: true + aws-secret-access-key: + description: 'AWS Secret Access Key' + required: true + github-sha: + description: 'Commit SHA to tag the image with' + default: ${{ github.sha }} + +outputs: + ecr_image: + description: 'Final ECR image URI' + value: ${{ steps.build.outputs.ecr_image }} + +runs: + using: composite + steps: + - name: Setup AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ inputs.aws-access-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + + - name: Login to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v1 + + - name: Download generated files + uses: actions/download-artifact@v4 + with: + name: generated-code + path: ${{ github.workspace }} + + - name: List generated files + shell: bash + run: | + echo "Listing .gen.ts and .gen.d.ts files:" + pwd + find . -name "*.gen.ts" -o -name "*.gen.d.ts" + + - name: Build and Push Docker Image + shell: bash + id: build + run: | + IMAGE_URI=${{ steps.ecr-login.outputs.registry }}/${{ inputs.image-name }}:${{ inputs.github-sha }} + docker build -t $IMAGE_URI -f ${{ inputs.dockerfile }} . + docker tag $IMAGE_URI ${{ steps.ecr-login.outputs.registry }}/${{ inputs.image-name }}:latest + docker push $IMAGE_URI + docker push ${{ steps.ecr-login.outputs.registry }}/${{ inputs.image-name }}:latest + # Safe base64 encode and output + IMAGE_URI_BASE64=$(echo -n "$IMAGE_URI" | base64 | tr -d '\n') + echo "ecr_image=$IMAGE_URI_BASE64" >> $GITHUB_OUTPUT diff --git a/.github/actions/setup-environment/action.yml b/.github/actions/setup-environment/action.yml new file mode 100644 index 0000000..3d2f6b3 --- /dev/null +++ b/.github/actions/setup-environment/action.yml @@ -0,0 +1,45 @@ +name: 'Setup Environment' +description: 'Setup environment for the project' +runs: + using: 'composite' + steps: + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Cache bun install + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules/.cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + + - run: bun install --frozen-lockfile + shell: bash + + - run: bun run prebuild + shell: bash + + - name: List generated files + shell: bash + run: | + echo "Listing .gen.ts and .gen.d.ts files:" + find . -name "*.gen.ts" -o -name "*.gen.d.ts" + + - name: Save generated types + uses: actions/upload-artifact@v4 + with: + name: generated-code + path: | + ${{ github.workspace }}/packages/functions/.pikku + ${{ github.workspace }}/packages/sdk/.generated + ${{ github.workspace }}/packages/sdk/.pikku + ${{ github.workspace }}/apps/website/pikku/* + ${{ github.workspace }}/apps/website/public/locales + ${{ github.workspace }}/apps/website/public/locales/* + include-hidden-files: true diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml new file mode 100644 index 0000000..d11e372 --- /dev/null +++ b/.github/workflows/develop.yml @@ -0,0 +1,26 @@ +name: Develop +run-name: The develop workflow + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - develop + workflow_dispatch: + +jobs: + setup: + name: Setup and Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup environment + uses: ./.github/actions/setup-environment + + - name: Run Tests + run: bun run test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..2744a8c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,52 @@ +name: Main +run-name: The main release workflow + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + setup: + name: Setup and Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup environment + uses: ./.github/actions/setup-environment + + - name: Run Tests + run: bun run test + + website-build: + name: Build Website + runs-on: ubuntu-latest + needs: setup + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # If your build generates code artifacts in prior jobs, keep this; + # otherwise you can remove it. + - name: Download generated files + uses: actions/download-artifact@v4 + with: + name: generated-code + path: ${{ github.workspace }} + continue-on-error: true + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + + - run: bun install --frozen-lockfile + + - name: Build NextJS Standalone App + run: bun run build:website diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bd6367 --- /dev/null +++ b/.gitignore @@ -0,0 +1,244 @@ +meta.json +openapi.json +openapi.yml +*.gen.ts + +locales +# Email locale sources are real committed source (drives EMAIL_LOCALES codegen), not build output. +!emails/locales/ +!emails/locales/** + +out +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +.generated + +*.hcl +pg_* + +generated +schemas +generated/* +.terraform +.serverless_nextjs +tsconfig.tsbuildinfo +assets/locales +.prod_db_creds +function.zip + +.public +.build +.deploy +.terraform +.serverless +serverless/index.js +serverless/index.js.map + +.next +.uploads +.tmp + +mobile/android/app/src/main/assets/fonts/ +.DS_Store + +*.css.d.ts + +Pods + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env.test +.env.local +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port +yarn-error.log + +.DS_Store +package-lock.json +dist +node_modules + +stats.json + +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifact +*.jsbundle + +# CocoaPods +/ios/Pods/ +VoxImplantDemo.xcworkspace +VoximplantDemo.xcworkspace + +android/app/google-services.json + + +.editorconfig + +# Pikku +.pikku + +# Tracked pikku client SDK (generated, but committed so it exists in fresh clones) +!packages/sdk/pikku/ +!packages/sdk/pikku/pikku-fetch.gen.ts + +meta.json +.e2e-runtime +e2e/tests/reports diff --git a/.pikku-runtime/dev.db b/.pikku-runtime/dev.db new file mode 100644 index 0000000..f6a2023 Binary files /dev/null and b/.pikku-runtime/dev.db differ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..fa51da2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6beef01 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,10 @@ +# Repo Guidance + +- Do not bypass `@pikku/mantine` i18n typing by wrapping final text in plain HTML elements such as `span` or `div`. +- Do not use wrapper-based "neutralization" to silence `I18nNode` errors. +- `Text` may contain plain numeric children like `{1}` when the value is a number, but do not brand digit literals like `'1'` as translation text. +- Fix the root issue instead: + - use `t('...')` for real UI copy + - improve shared component typings/helpers when needed + - use `asI18n(...)` only in an explicit, deliberate pass +- If the correct fix is not clear, stop and ask before introducing a workaround. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..467f9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Vlandor Ltd + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a82cdd --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Hey Germany + +Hey Germany is a platform that helps immigrants find job opportunities in Germany. + +## Requirements + +- **Node.js 18** + +## Setup + +To get started, run: + +```bash +yarn install +yarn prebuild +``` + +Local SQLite defaults are defined in [packages/functions/src/local-db.ts](/Users/yasser/git/heygermany/packages/functions/src/local-db.ts:1). + +## Apps + +### `apps/cli` + +This app tests the Pikku fetch functionality. + +### `apps/webapp` + +A Next.js App Router integrated with Pikku. + +## Runtime + +The backend runs through `pikku dev` locally and Fabric in production. + +## Packages + +### `functions` + +Contains all the Pikku routes, functions, and services. + +### `sdk` + +Contains shared types used by both the backend and frontend. + +## Database + +SQLite migrations live in `db/sqlite`, preserved Postgres migrations live in `db/postgres`, and local migration runs should use `pikku db migrate`. + +--- + +Feel free to explore each package and customize the starter to fit your project's needs. Happy coding! diff --git a/apps/website/.gitignore b/apps/website/.gitignore new file mode 100644 index 0000000..1dd45b2 --- /dev/null +++ b/apps/website/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Sentry Config File +.env.sentry-build-plugin diff --git a/apps/website/README.md b/apps/website/README.md new file mode 100644 index 0000000..afaecd0 --- /dev/null +++ b/apps/website/README.md @@ -0,0 +1,15 @@ +This app runs on TanStack Start with Vite. + +## Commands + +```bash +yarn dev +yarn build +yarn start +``` + +## Structure + +- `src/routes`: TanStack Start file routes +- `views`: route components and page-level React views +- `components`: shared UI and feature components diff --git a/apps/website/components/LoginComponent.module.css b/apps/website/components/LoginComponent.module.css new file mode 100644 index 0000000..b0b78cf --- /dev/null +++ b/apps/website/components/LoginComponent.module.css @@ -0,0 +1,8 @@ +.wrapper { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-size: cover; + background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80); +} \ No newline at end of file diff --git a/apps/website/components/LoginComponent.tsx b/apps/website/components/LoginComponent.tsx new file mode 100644 index 0000000..016b75b --- /dev/null +++ b/apps/website/components/LoginComponent.tsx @@ -0,0 +1,119 @@ +import { + Alert, + Button, + Card, + Checkbox, + Paper, + PasswordInput, + Text, + TextInput, + Title, +} from '@pikku/mantine/core'; +import { useForm } from 'react-hook-form'; +import { useI18n } from '@/context/i18n-provider'; +import classes from './LoginComponent.module.css'; + +export type LoginFormData = { + email: string; + password: string; + rememberMe: boolean; +}; + +export interface LoginComponentProps { + namespace: 'backoffice' | 'company'; + mutation: { + mutateAsync: (data: { email: string; password: string }) => Promise; + isPending: boolean; + error: Error | null; + }; +} + +export function LoginComponent({ namespace, mutation }: LoginComponentProps) { + const t = useI18n(); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + email: '', + password: '', + rememberMe: false, + }, + }); + + const onSubmit = async (data: LoginFormData) => { + await mutation.mutateAsync({ + email: data.email, + password: data.password, + }) + }; + + return ( +
+ + + {t(`login.${namespace}.title` as any)} + + +
+ {mutation.error && ( + + + {t('login.error.message')} + + + )} + + + + + + +
+
+ ); +} diff --git a/apps/website/components/NotFoundTitle.module.css b/apps/website/components/NotFoundTitle.module.css new file mode 100644 index 0000000..9362962 --- /dev/null +++ b/apps/website/components/NotFoundTitle.module.css @@ -0,0 +1,35 @@ +.root { + padding-top: 80px; + padding-bottom: 80px; +} + +.label { + text-align: center; + font-weight: 500; + font-size: 38px; + line-height: 1; + margin-bottom: calc(1.5 * var(--mantine-spacing-xl)); + color: var(--mantine-color-gray-2); + + @media (max-width: $mantine-breakpoint-sm) { + font-size: 32px; + } +} + +.description { + max-width: 500px; + margin: auto; + margin-top: var(--mantine-spacing-xl); + margin-bottom: calc(1.5 * var(--mantine-spacing-xl)); +} + +.title { + font-family: 'Outfit', var(--mantine-font-family); + text-align: center; + font-weight: 500; + font-size: 38px; + + @media (max-width: $mantine-breakpoint-sm) { + font-size: 32px; + } +} \ No newline at end of file diff --git a/apps/website/components/NotFoundTitle.tsx b/apps/website/components/NotFoundTitle.tsx new file mode 100644 index 0000000..a48a2f3 --- /dev/null +++ b/apps/website/components/NotFoundTitle.tsx @@ -0,0 +1,30 @@ +import { Button, Container, Group, Text, Title } from '@pikku/mantine/core'; +import { DEFAULT_LOCALE } from '@/config'; +import { useI18n } from '@/context/i18n-provider'; +import { useNavigate, useParams } from '@tanstack/react-router'; +import classes from './NotFoundTitle.module.css'; + +export function NotFoundTitle() { + const t = useI18n(); + const navigate = useNavigate(); + const { lang } = useParams({ strict: false }) as { lang?: string }; + + const handleGoHome = () => { + navigate({ to: lang ? `/${lang}` : `/${DEFAULT_LOCALE}` }); + }; + + return ( + +
404
+ {t('notfound.title')} + + {t('notfound.description')} + + + + +
+ ); +} diff --git a/apps/website/components/backoffice/ApplicantStatusSelect.tsx b/apps/website/components/backoffice/ApplicantStatusSelect.tsx new file mode 100644 index 0000000..d619ae8 --- /dev/null +++ b/apps/website/components/backoffice/ApplicantStatusSelect.tsx @@ -0,0 +1,43 @@ +import { Select } from '@pikku/mantine/core'; +import type { DB } from '@heygermany/sdk'; +import { ApplicantStatuses } from '@heygermany/sdk'; +import { useI18n } from '@/context/i18n-provider'; + +interface ApplicantStatusSelectProps { + value: DB.ApplicantStatus | null; + onChange: (value: DB.ApplicantStatus | null) => void; + isValid: boolean; +} + +export const ApplicantStatusSelect: React.FC = ({ + value, + onChange, + isValid +}) => { + const t = useI18n(); + const allStatuses = Object.values(ApplicantStatuses); + const preferredOrder: DB.ApplicantStatus[] = [ + ApplicantStatuses.INITIAL, + ApplicantStatuses.PENDING, + ApplicantStatuses.INVALID, + ApplicantStatuses.COMPLETE, + ]; + + const orderedStatuses = [ + ...preferredOrder.filter((status) => allStatuses.includes(status)), + ...allStatuses.filter((status) => !preferredOrder.includes(status)), + ]; + + return ( +