Compare commits

...

37 Commits

Author SHA1 Message Date
Alexis Bruneteau
b83c7a7d6d feat(migration): migrate from Angular 20 to Next.js 15
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 1m31s
Complete framework migration from Angular to Next.js with full feature parity.

## What Changed
- Migrated from Angular 20 to Next.js 15 with App Router
- Replaced Angular components with React functional components
- Implemented React Context API for state management (replacing RxJS)
- Integrated React Hook Form for form handling
- Added shadcn/ui component library
- Configured Tailwind CSS 4.1 with @tailwindcss/postcss
- Implemented JWT authentication with middleware protection

## Core Features Implemented
-  User registration and login with validation
-  JWT token authentication with localStorage
-  Protected dashboard route with middleware
-  Portfolio listing with status indicators
-  Create portfolio functionality
-  ZIP file upload with progress tracking
-  Portfolio deployment
-  Responsive design with Tailwind CSS

## Technical Stack
- Framework: Next.js 15 (App Router)
- Language: TypeScript 5.8
- Styling: Tailwind CSS 4.1
- UI Components: shadcn/ui + Lucide icons
- State: React Context API + hooks
- Forms: React Hook Form
- API Client: Native fetch with custom wrapper

## File Structure
- /app - Next.js pages (landing, login, register, dashboard)
- /components - React components (ui primitives, auth provider)
- /lib - API client, types, utilities
- /hooks - Custom hooks (useAuth, usePortfolios)
- /middleware.ts - Route protection
- /angular-backup - Preserved Angular source code

## API Compatibility
- All backend endpoints remain unchanged
- JWT Bearer token authentication preserved
- API response format maintained

## Build Output
- Production build: 7 routes compiled successfully
- Bundle size: ~102kB shared JS (optimized)
- First Load JS: 103-126kB per route

## Documentation
- Updated README.md with Next.js setup guide
- Created OpenSpec proposal in openspec/changes/migrate-to-nextjs-launchui/
- Added project context in openspec/project.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 00:34:43 +02:00
Alexis Bruneteau
609940cafe env
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 38s
2025-06-05 03:30:17 +02:00
Alexis Bruneteau
49e1bcac81 working good
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 38s
2025-06-04 21:52:54 +02:00
Alexis Bruneteau
1fdf7516a5 tag 2025-06-04 03:32:57 +02:00
Alexis Bruneteau
d5c4a99281 set production API
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 47s
2025-06-02 14:32:07 +02:00
Alexis Bruneteau
e785ea0ea6 fixed ingress
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 38s
2025-06-02 12:17:44 +02:00
Alexis Bruneteau
cd89d07f70 changed docker image tag
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 40s
2025-06-02 10:13:46 +02:00
Alexis Bruneteau
3ade4d1bd1 changed icon
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 38s
2025-06-02 10:04:42 +02:00
Alexis Bruneteau
120fc26d01 DNS
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 38s
2025-06-01 15:34:02 +02:00
Alexis Bruneteau
a55a295164 pre-alpha test
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 39s
2025-06-01 14:04:20 +02:00
Alexis Bruneteau
e616a400ed optimized Dockerfile
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 38s
2025-06-01 03:14:46 +02:00
Alexis Bruneteau
8acf22a953 optimized Dockerfile
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 40s
2025-06-01 03:07:08 +02:00
Alexis Bruneteau
295c15f03b landing
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 1m7s
2025-06-01 03:03:04 +02:00
Alexis Bruneteau
ee5daffdba landing
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 36s
2025-06-01 03:01:58 +02:00
Alexis Bruneteau
3849e2d0dc tag
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 1m10s
2025-05-31 18:14:35 +02:00
Alexis Bruneteau
46e5797d85 tag
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 1m19s
2025-05-31 18:13:16 +02:00
Alexis Bruneteau
43ddc32e17 tag
Some checks failed
Build and Deploy to k3s / build-and-deploy (push) Failing after 1m25s
2025-05-31 18:12:23 +02:00
Alexis Bruneteau
40793eceab tag 2025-05-31 18:10:58 +02:00
Alexis Bruneteau
4d2b176816 tag 2025-05-31 18:00:35 +02:00
Alexis Bruneteau
85ab22d566 dingz 2025-05-31 17:58:30 +02:00
Alexis Bruneteau
97520d404a retesting 2025-05-31 17:47:59 +02:00
Alexis Bruneteau
225c0654cc retesting 2025-05-31 17:44:22 +02:00
Alexis Bruneteau
00f4158d57 retesting 2025-05-31 17:41:54 +02:00
Alexis Bruneteau
f2c2b55cb1 testing 2025-05-31 17:36:34 +02:00
Alexis Bruneteau
24c382d520 debugging 2025-05-31 17:23:30 +02:00
Alexis Bruneteau
128d87636a tag 2025-05-31 17:17:16 +02:00
Alexis Bruneteau
54083e4233 tag 2025-05-31 17:15:36 +02:00
Alexis Bruneteau
fe14d9f790 tag 2025-05-31 16:51:52 +02:00
Alexis Bruneteau
6f76ba2f6e tag 2025-05-31 16:09:37 +02:00
Alexis Bruneteau
0643d4fb08 tag 2025-05-31 16:08:09 +02:00
Alexis Bruneteau
7e0cd37851 tag 2025-05-31 15:54:58 +02:00
Alexis Bruneteau
e378e7fa11 tag 2025-05-31 15:33:13 +02:00
Alexis Bruneteau
1a862b1efc fix 2025-05-31 03:38:25 +02:00
Alexis Bruneteau
9d25fbc935 fix 2025-05-31 03:28:06 +02:00
Alexis Bruneteau
c03360713f fix 2025-05-31 03:11:55 +02:00
Alexis Bruneteau
ae2b3b106a waouh 2025-05-31 02:43:51 +02:00
Alexis Bruneteau
70f799ea73 first commit 2025-05-31 01:02:43 +02:00
209 changed files with 17779 additions and 8543 deletions

View File

@ -0,0 +1,23 @@
---
name: OpenSpec: Apply
description: Implement an approved OpenSpec change and keep tasks in sync.
category: OpenSpec
tags: [openspec, apply]
---
<!-- OPENSPEC:START -->
**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
**Steps**
Track these steps as TODOs and complete them one by one.
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
**Reference**
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
<!-- OPENSPEC:END -->

View File

@ -0,0 +1,21 @@
---
name: OpenSpec: Archive
description: Archive a deployed OpenSpec change and update specs.
category: OpenSpec
tags: [openspec, archive]
---
<!-- OPENSPEC:START -->
**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
**Steps**
1. Identify the requested change ID (via the prompt or `openspec list`).
2. Run `openspec archive <id> --yes` to let the CLI move the change and apply spec updates without prompts (use `--skip-specs` only for tooling-only work).
3. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
4. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
**Reference**
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
<!-- OPENSPEC:END -->

View File

@ -0,0 +1,27 @@
---
name: OpenSpec: Proposal
description: Scaffold a new OpenSpec change and validate strictly.
category: OpenSpec
tags: [openspec, change]
---
<!-- OPENSPEC:START -->
**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
**Steps**
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
5. Draft spec deltas in `changes/<id>/specs/<capability>/spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant.
6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
**Reference**
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
<!-- OPENSPEC:END -->

1
.env.local Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=http://localhost:8000/api

1
.env.production Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=https://portfolio-host.com/api

View File

@ -0,0 +1,70 @@
name: Build and Deploy to k3s
on:
push:
tags:
- 'PRE_ALPHA*'
jobs:
build-and-deploy:
env:
KUBECONFIG: ~/.kube/config
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Build Angular app
run: npm run build --prod
- name: Build Docker image
run: |
docker build -t ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend:${{ github.sha }} .
docker tag ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend:${{ github.sha }} ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend:latest
- name: Login to Container Registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_URL }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Push Docker image
run: |
docker push ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend:${{ github.sha }}
docker push ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend:latest
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: 'latest'
- name: Configure kubectl
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- name: Check Config
run: |
cat ~/.kube/config
- name: Validate kubeconfig
run: |
if ! kubectl version --client && kubectl cluster-info --kubeconfig ~/.kube/config; then
echo "❌ Failed to connect to cluster"
exit 1
fi
- name: Deploy to k3s
run: |
kubectl apply -k deploy/k3s/alpha --kubeconfig ~/.kube/config
kubectl set image deployment/hosting-frontend \
hosting-frontend=${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend:${{ github.sha }} \
-n hosting-alpha --kubeconfig ~/.kube/config
kubectl rollout status deployment/hosting-frontend -n hosting-alpha --kubeconfig ~/.kube/config

View File

@ -0,0 +1,67 @@
name: Build and Deploy to k3s
on:
push:
tags:
- 'PROD*'
jobs:
build-and-deploy:
env:
KUBECONFIG: ~/.kube/config
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Build Angular app
run: npm run build --prod
- name: Build Docker image
run: |
docker build -t ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend-prod:${{ github.sha }} .
docker tag ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend-prod:${{ github.sha }} ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend-prod:latest
- name: Login to Container Registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_URL }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Push Docker image
run: |
docker push ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend-prod:${{ github.sha }}
docker push ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend-prod:latest
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: 'latest'
- name: Configure kubectl
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- name: Validate kubeconfig
run: |
if ! kubectl version --client && kubectl cluster-info --kubeconfig ~/.kube/config; then
echo "❌ Failed to connect to cluster"
exit 1
fi
- name: Deploy to k3s
run: |
kubectl apply -k deploy/k3s/prod --kubeconfig ~/.kube/config
kubectl set image deployment/hosting-frontend \
hosting-frontend=${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/hosting-frontend-prod:${{ github.sha }} \
-n hosting --kubeconfig ~/.kube/config
kubectl rollout status deployment/hosting-frontend -n hosting --kubeconfig ~/.kube/config

View File

@ -0,0 +1,15 @@
{
"pages": {
"/layout": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/css/app/layout.css",
"static/chunks/app/layout.js"
],
"/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/page.js"
]
}
}

20
.next/build-manifest.json Normal file
View File

@ -0,0 +1,20 @@
{
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [
"static/development/_buildManifest.js",
"static/development/_ssgManifest.js"
],
"rootMainFiles": [
"static/chunks/webpack.js",
"static/chunks/main-app.js"
],
"rootMainFilesTree": {},
"pages": {
"/_app": []
},
"ampFirstPages": []
}

1
.next/cache/.previewinfo vendored Normal file
View File

@ -0,0 +1 @@
{"previewModeId":"dc559f34b2cfb8e471f5cf7c4303e624","previewModeSigningKey":"cbfcb5c64d2055dbda1bfdbc6833ad47f57d6fb250a5de05c1dc43c23f26d6b1","previewModeEncryptionKey":"0ac99f44e54b21dc78776676abda88910973e01c15da70460817db51a4491fdd","expireAt":1761863112139}

1
.next/cache/.rscinfo vendored Normal file
View File

@ -0,0 +1 @@
{"encryption.key":"UJCuqkc43Q1Tu8qgslshb0xL4QAUycNhcPIjjC8lUyo=","encryption.expire_at":1761863112040}

1
.next/cache/.tsbuildinfo vendored Normal file

File diff suppressed because one or more lines are too long

1
.next/cache/next-devtools-config.json vendored Normal file
View File

@ -0,0 +1 @@
{}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
.next/package.json Normal file
View File

@ -0,0 +1 @@
{"type": "commonjs"}

View File

@ -0,0 +1,11 @@
{
"version": 4,
"routes": {},
"dynamicRoutes": {},
"notFoundRoutes": [],
"preview": {
"previewModeId": "b519e1f670f54aba65a44541ac258fa1",
"previewModeSigningKey": "357de2109d161118a84c195d2910555373bfa1683d172ae54a76574b720b4802",
"previewModeEncryptionKey": "85cbb6533f9cf3ef2e23ca4286fc7869149638eadc425c8a643447926c624384"
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{"version":3,"caseSensitive":false,"basePath":"","rewrites":{"beforeFiles":[],"afterFiles":[],"fallback":[]},"redirects":[{"source":"/:path+/","destination":"/:path+","permanent":true,"internal":true,"regex":"^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))\\/$"}],"headers":[]}

View File

@ -0,0 +1,3 @@
{
"/page": "app/page.js"
}

316
.next/server/app/page.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"

View File

@ -0,0 +1,22 @@
globalThis.__BUILD_MANIFEST = {
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [],
"rootMainFiles": [
"static/chunks/webpack.js",
"static/chunks/main-app.js"
],
"rootMainFilesTree": {},
"pages": {
"/_app": []
},
"ampFirstPages": []
};
globalThis.__BUILD_MANIFEST.lowPriorityFiles = [
"/static/" + process.env.__NEXT_BUILD_ID + "/_buildManifest.js",
,"/static/" + process.env.__NEXT_BUILD_ID + "/_ssgManifest.js",
];

View File

@ -0,0 +1,6 @@
{
"version": 3,
"middleware": {},
"functions": {},
"sortedMiddleware": []
}

View File

@ -0,0 +1 @@
self.__REACT_LOADABLE_MANIFEST="{}"

View File

@ -0,0 +1 @@
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{\"/home/sorti/projects/hosting-frontend/app/layout\":[\"static/media/e4af272ccee01ff0-s.p.woff2\"]},\"appUsingSizeAdjust\":true,\"pagesUsingSizeAdjust\":false}"

View File

@ -0,0 +1 @@
{"pages":{},"app":{"/home/sorti/projects/hosting-frontend/app/layout":["static/media/e4af272ccee01ff0-s.p.woff2"]},"appUsingSizeAdjust":true,"pagesUsingSizeAdjust":false}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}"

View File

@ -0,0 +1,5 @@
{
"node": {},
"edge": {},
"encryptionKey": "UJCuqkc43Q1Tu8qgslshb0xL4QAUycNhcPIjjC8lUyo="
}

View File

@ -0,0 +1,55 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
exports.id = "vendor-chunks/@swc";
exports.ids = ["vendor-chunks/@swc"];
exports.modules = {
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js":
/*!**************************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js ***!
\**************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_base)\n/* harmony export */ });\nfunction _class_private_field_loose_base(receiver, privateKey) {\n if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {\n throw new TypeError(\"attempted to use private field on non-instance\");\n }\n\n return receiver;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlLmpzIiwibWFwcGluZ3MiOiI7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ2dEIiwic291cmNlcyI6WyIvaG9tZS9zb3J0aS9wcm9qZWN0cy9ob3N0aW5nLWZyb250ZW5kL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL19jbGFzc19wcml2YXRlX2ZpZWxkX2xvb3NlX2Jhc2UuanMiXSwic291cmNlc0NvbnRlbnQiOlsiZnVuY3Rpb24gX2NsYXNzX3ByaXZhdGVfZmllbGRfbG9vc2VfYmFzZShyZWNlaXZlciwgcHJpdmF0ZUtleSkge1xuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHJlY2VpdmVyLCBwcml2YXRlS2V5KSkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKFwiYXR0ZW1wdGVkIHRvIHVzZSBwcml2YXRlIGZpZWxkIG9uIG5vbi1pbnN0YW5jZVwiKTtcbiAgICB9XG5cbiAgICByZXR1cm4gcmVjZWl2ZXI7XG59XG5leHBvcnQgeyBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOlswXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js":
/*!*************************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js ***!
\*************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_key)\n/* harmony export */ });\nvar id = 0;\n\nfunction _class_private_field_loose_key(name) {\n return \"__private_\" + id++ + \"_\" + name;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkuanMiLCJtYXBwaW5ncyI6Ijs7OztBQUFBOztBQUVBO0FBQ0E7QUFDQTtBQUMrQyIsInNvdXJjZXMiOlsiL2hvbWUvc29ydGkvcHJvamVjdHMvaG9zdGluZy1mcm9udGVuZC9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkuanMiXSwic291cmNlc0NvbnRlbnQiOlsidmFyIGlkID0gMDtcblxuZnVuY3Rpb24gX2NsYXNzX3ByaXZhdGVfZmllbGRfbG9vc2Vfa2V5KG5hbWUpIHtcbiAgICByZXR1cm4gXCJfX3ByaXZhdGVfXCIgKyBpZCsrICsgXCJfXCIgKyBuYW1lO1xufVxuZXhwb3J0IHsgX2NsYXNzX3ByaXZhdGVfZmllbGRfbG9vc2Vfa2V5IGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOlswXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
/*!*******************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.js ***!
\*******************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7OztBQUFBO0FBQ0EsMkNBQTJDO0FBQzNDO0FBQ3lDIiwic291cmNlcyI6WyIvaG9tZS9zb3J0aS9wcm9qZWN0cy9ob3N0aW5nLWZyb250ZW5kL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL19pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdC5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJmdW5jdGlvbiBfaW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQob2JqKSB7XG4gICAgcmV0dXJuIG9iaiAmJiBvYmouX19lc01vZHVsZSA/IG9iaiA6IHsgZGVmYXVsdDogb2JqIH07XG59XG5leHBvcnQgeyBfaW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQgYXMgXyB9O1xuIl0sIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6WzBdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js":
/*!********************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_wildcard.js ***!
\********************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_wildcard)\n/* harmony export */ });\nfunction _getRequireWildcardCache(nodeInterop) {\n if (typeof WeakMap !== \"function\") return null;\n\n var cacheBabelInterop = new WeakMap();\n var cacheNodeInterop = new WeakMap();\n\n return (_getRequireWildcardCache = function(nodeInterop) {\n return nodeInterop ? cacheNodeInterop : cacheBabelInterop;\n })(nodeInterop);\n}\nfunction _interop_require_wildcard(obj, nodeInterop) {\n if (!nodeInterop && obj && obj.__esModule) return obj;\n if (obj === null || typeof obj !== \"object\" && typeof obj !== \"function\") return { default: obj };\n\n var cache = _getRequireWildcardCache(nodeInterop);\n\n if (cache && cache.has(obj)) return cache.get(obj);\n\n var newObj = { __proto__: null };\n var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;\n\n for (var key in obj) {\n if (key !== \"default\" && Object.prototype.hasOwnProperty.call(obj, key)) {\n var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;\n if (desc && (desc.get || desc.set)) Object.defineProperty(newObj, key, desc);\n else newObj[key] = obj[key];\n }\n }\n\n newObj.default = obj;\n\n if (cache) cache.set(obj, newObj);\n\n return newObj;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkLmpzIiwibWFwcGluZ3MiOiI7Ozs7QUFBQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBO0FBQ0E7QUFDQSx1RkFBdUY7O0FBRXZGOztBQUVBOztBQUVBLG1CQUFtQjtBQUNuQjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTs7QUFFQTtBQUNBO0FBQzBDIiwic291cmNlcyI6WyIvaG9tZS9zb3J0aS9wcm9qZWN0cy9ob3N0aW5nLWZyb250ZW5kL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL19pbnRlcm9wX3JlcXVpcmVfd2lsZGNhcmQuanMiXSwic291cmNlc0NvbnRlbnQiOlsiZnVuY3Rpb24gX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlKG5vZGVJbnRlcm9wKSB7XG4gICAgaWYgKHR5cGVvZiBXZWFrTWFwICE9PSBcImZ1bmN0aW9uXCIpIHJldHVybiBudWxsO1xuXG4gICAgdmFyIGNhY2hlQmFiZWxJbnRlcm9wID0gbmV3IFdlYWtNYXAoKTtcbiAgICB2YXIgY2FjaGVOb2RlSW50ZXJvcCA9IG5ldyBXZWFrTWFwKCk7XG5cbiAgICByZXR1cm4gKF9nZXRSZXF1aXJlV2lsZGNhcmRDYWNoZSA9IGZ1bmN0aW9uKG5vZGVJbnRlcm9wKSB7XG4gICAgICAgIHJldHVybiBub2RlSW50ZXJvcCA/IGNhY2hlTm9kZUludGVyb3AgOiBjYWNoZUJhYmVsSW50ZXJvcDtcbiAgICB9KShub2RlSW50ZXJvcCk7XG59XG5mdW5jdGlvbiBfaW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkKG9iaiwgbm9kZUludGVyb3ApIHtcbiAgICBpZiAoIW5vZGVJbnRlcm9wICYmIG9iaiAmJiBvYmouX19lc01vZHVsZSkgcmV0dXJuIG9iajtcbiAgICBpZiAob2JqID09PSBudWxsIHx8IHR5cGVvZiBvYmogIT09IFwib2JqZWN0XCIgJiYgdHlwZW9mIG9iaiAhPT0gXCJmdW5jdGlvblwiKSByZXR1cm4geyBkZWZhdWx0OiBvYmogfTtcblxuICAgIHZhciBjYWNoZSA9IF9nZXRSZXF1aXJlV2lsZGNhcmRDYWNoZShub2RlSW50ZXJvcCk7XG5cbiAgICBpZiAoY2FjaGUgJiYgY2FjaGUuaGFzKG9iaikpIHJldHVybiBjYWNoZS5nZXQob2JqKTtcblxuICAgIHZhciBuZXdPYmogPSB7IF9fcHJvdG9fXzogbnVsbCB9O1xuICAgIHZhciBoYXNQcm9wZXJ0eURlc2NyaXB0b3IgPSBPYmplY3QuZGVmaW5lUHJvcGVydHkgJiYgT2JqZWN0LmdldE93blByb3BlcnR5RGVzY3JpcHRvcjtcblxuICAgIGZvciAodmFyIGtleSBpbiBvYmopIHtcbiAgICAgICAgaWYgKGtleSAhPT0gXCJkZWZhdWx0XCIgJiYgT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKG9iaiwga2V5KSkge1xuICAgICAgICAgICAgdmFyIGRlc2MgPSBoYXNQcm9wZXJ0eURlc2NyaXB0b3IgPyBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKG9iaiwga2V5KSA6IG51bGw7XG4gICAgICAgICAgICBpZiAoZGVzYyAmJiAoZGVzYy5nZXQgfHwgZGVzYy5zZXQpKSBPYmplY3QuZGVmaW5lUHJvcGVydHkobmV3T2JqLCBrZXksIGRlc2MpO1xuICAgICAgICAgICAgZWxzZSBuZXdPYmpba2V5XSA9IG9ialtrZXldO1xuICAgICAgICB9XG4gICAgfVxuXG4gICAgbmV3T2JqLmRlZmF1bHQgPSBvYmo7XG5cbiAgICBpZiAoY2FjaGUpIGNhY2hlLnNldChvYmosIG5ld09iaik7XG5cbiAgICByZXR1cm4gbmV3T2JqO1xufVxuZXhwb3J0IHsgX2ludGVyb3BfcmVxdWlyZV93aWxkY2FyZCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbMF0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js\n");
/***/ })
};
;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
exports.id = "vendor-chunks/clsx";
exports.ids = ["vendor-chunks/clsx"];
exports.modules = {
/***/ "(rsc)/./node_modules/clsx/dist/clsx.mjs":
/*!*****************************************!*\
!*** ./node_modules/clsx/dist/clsx.mjs ***!
\*****************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ clsx: () => (/* binding */ clsx),\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nfunction r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (clsx);//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHJzYykvLi9ub2RlX21vZHVsZXMvY2xzeC9kaXN0L2Nsc3gubWpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQUEsY0FBYyxhQUFhLCtDQUErQyxnREFBZ0QsZUFBZSxRQUFRLElBQUksMENBQTBDLHlDQUF5QyxTQUFnQixnQkFBZ0Isd0NBQXdDLElBQUksbURBQW1ELFNBQVMsaUVBQWUsSUFBSSIsInNvdXJjZXMiOlsiL2hvbWUvc29ydGkvcHJvamVjdHMvaG9zdGluZy1mcm9udGVuZC9ub2RlX21vZHVsZXMvY2xzeC9kaXN0L2Nsc3gubWpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIHIoZSl7dmFyIHQsZixuPVwiXCI7aWYoXCJzdHJpbmdcIj09dHlwZW9mIGV8fFwibnVtYmVyXCI9PXR5cGVvZiBlKW4rPWU7ZWxzZSBpZihcIm9iamVjdFwiPT10eXBlb2YgZSlpZihBcnJheS5pc0FycmF5KGUpKXt2YXIgbz1lLmxlbmd0aDtmb3IodD0wO3Q8bzt0KyspZVt0XSYmKGY9cihlW3RdKSkmJihuJiYobis9XCIgXCIpLG4rPWYpfWVsc2UgZm9yKGYgaW4gZSllW2ZdJiYobiYmKG4rPVwiIFwiKSxuKz1mKTtyZXR1cm4gbn1leHBvcnQgZnVuY3Rpb24gY2xzeCgpe2Zvcih2YXIgZSx0LGY9MCxuPVwiXCIsbz1hcmd1bWVudHMubGVuZ3RoO2Y8bztmKyspKGU9YXJndW1lbnRzW2ZdKSYmKHQ9cihlKSkmJihuJiYobis9XCIgXCIpLG4rPXQpO3JldHVybiBufWV4cG9ydCBkZWZhdWx0IGNsc3g7Il0sIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6WzBdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(rsc)/./node_modules/clsx/dist/clsx.mjs\n");
/***/ })
};
;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
self.__BUILD_MANIFEST = (function(a){return {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},__routerFilterStatic:a,__routerFilterDynamic:a,sortedPages:["\u002F_app"]}}(void 0));self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()

View File

@ -0,0 +1 @@
self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
{"c":["app/layout","webpack"],"r":[],"m":[]}

View File

@ -0,0 +1 @@
{"c":[],"r":[],"m":[]}

View File

@ -0,0 +1,22 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("app/layout",{
/***/ "(app-pages-browser)/./app/globals.css":
/*!*************************!*\
!*** ./app/globals.css ***!
\*************************/
/***/ ((module, __webpack_exports__, __webpack_require__) => {
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (\"f7e2466089dd\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL2FwcC9nbG9iYWxzLmNzcyIsIm1hcHBpbmdzIjoiOzs7O0FBQUEsaUVBQWUsY0FBYztBQUM3QixJQUFJLElBQVUsSUFBSSxpQkFBaUIiLCJzb3VyY2VzIjpbIi9ob21lL3NvcnRpL3Byb2plY3RzL2hvc3RpbmctZnJvbnRlbmQvYXBwL2dsb2JhbHMuY3NzIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBkZWZhdWx0IFwiZjdlMjQ2NjA4OWRkXCJcbmlmIChtb2R1bGUuaG90KSB7IG1vZHVsZS5ob3QuYWNjZXB0KCkgfVxuIl0sIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(app-pages-browser)/./app/globals.css\n"));
/***/ })
});

View File

@ -0,0 +1,12 @@
"use strict";
self["webpackHotUpdate_N_E"]("webpack",{},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("8e3f0ede0ae7b4ef")
/******/ })();
/******/
/******/ }
)
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJpZ25vcmVMaXN0IjpbMF0sIm1hcHBpbmdzIjoiQUFBQSIsInNvdXJjZXMiOlsid2VicGFjay1pbnRlcm5hbDovL25leHRqcy93ZWJwYWNrLmpzIl0sInNvdXJjZXNDb250ZW50IjpbIi8vIFRoaXMgc291cmNlIHdhcyBnZW5lcmF0ZWQgYnkgTmV4dC5qcyBiYXNlZCBvZmYgb2YgdGhlIGdlbmVyYXRlZCBXZWJwYWNrIHJ1bnRpbWUuXG4vLyBUaGUgbWFwcGluZ3MgYXJlIGluY29ycmVjdC5cbi8vIFRvIGdldCB0aGUgY29ycmVjdCBsaW5lL2NvbHVtbiBtYXBwaW5ncywgdHVybiBvZmYgc291cmNlbWFwcyBpbiB5b3VyIGRlYnVnZ2VyLlxuXG5zZWxmW1wid2VicGFja0hvdFVwZGF0ZV9OX0VcIl0oXCJ3ZWJwYWNrXCIse30sXG4vKioqKioqLyBmdW5jdGlvbihfX3dlYnBhY2tfcmVxdWlyZV9fKSB7IC8vIHdlYnBhY2tSdW50aW1lTW9kdWxlc1xuLyoqKioqKi8gLyogd2VicGFjay9ydW50aW1lL2dldEZ1bGxIYXNoICovXG4vKioqKioqLyAoKCkgPT4ge1xuLyoqKioqKi8gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLmggPSAoKSA9PiAoXCI4ZTNmMGVkZTBhZTdiNGVmXCIpXG4vKioqKioqLyB9KSgpO1xuLyoqKioqKi8gXG4vKioqKioqLyB9XG4pIl19
;

10
.next/trace Normal file

File diff suppressed because one or more lines are too long

84
.next/types/app/layout.ts Normal file
View File

@ -0,0 +1,84 @@
// File: /home/sorti/projects/hosting-frontend/app/layout.tsx
import * as entry from '../../../app/layout.js'
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
type TEntry = typeof import('../../../app/layout.js')
type SegmentParams<T extends Object = any> = T extends Record<string, any>
? { [K in keyof T]: T[K] extends string ? string | string[] | undefined : never }
: T
// Check that the entry is a valid entry
checkFields<Diff<{
default: Function
config?: {}
generateStaticParams?: Function
revalidate?: RevalidateRange<TEntry> | false
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
dynamicParams?: boolean
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
maxDuration?: number
metadata?: any
generateMetadata?: Function
viewport?: any
generateViewport?: Function
experimental_ppr?: boolean
}, TEntry, ''>>()
// Check the prop type of the entry function
checkFields<Diff<LayoutProps, FirstArg<TEntry['default']>, 'default'>>()
// Check the arguments and return type of the generateMetadata function
if ('generateMetadata' in entry) {
checkFields<Diff<LayoutProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
}
// Check the arguments and return type of the generateViewport function
if ('generateViewport' in entry) {
checkFields<Diff<LayoutProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
}
// Check the arguments and return type of the generateStaticParams function
if ('generateStaticParams' in entry) {
checkFields<Diff<{ params: SegmentParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
}
export interface PageProps {
params?: Promise<SegmentParams>
searchParams?: Promise<any>
}
export interface LayoutProps {
children?: React.ReactNode
params?: Promise<SegmentParams>
}
// =============
// Utility types
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
function checkFields<_ extends { [k in keyof any]: never }>() {}
// https://github.com/sindresorhus/type-fest
type Numeric = number | bigint
type Zero = 0 | 0n
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'

84
.next/types/app/page.ts Normal file
View File

@ -0,0 +1,84 @@
// File: /home/sorti/projects/hosting-frontend/app/page.tsx
import * as entry from '../../../app/page.js'
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
type TEntry = typeof import('../../../app/page.js')
type SegmentParams<T extends Object = any> = T extends Record<string, any>
? { [K in keyof T]: T[K] extends string ? string | string[] | undefined : never }
: T
// Check that the entry is a valid entry
checkFields<Diff<{
default: Function
config?: {}
generateStaticParams?: Function
revalidate?: RevalidateRange<TEntry> | false
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
dynamicParams?: boolean
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
maxDuration?: number
metadata?: any
generateMetadata?: Function
viewport?: any
generateViewport?: Function
experimental_ppr?: boolean
}, TEntry, ''>>()
// Check the prop type of the entry function
checkFields<Diff<PageProps, FirstArg<TEntry['default']>, 'default'>>()
// Check the arguments and return type of the generateMetadata function
if ('generateMetadata' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
}
// Check the arguments and return type of the generateViewport function
if ('generateViewport' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
}
// Check the arguments and return type of the generateStaticParams function
if ('generateStaticParams' in entry) {
checkFields<Diff<{ params: SegmentParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
}
export interface PageProps {
params?: Promise<SegmentParams>
searchParams?: Promise<any>
}
export interface LayoutProps {
children?: React.ReactNode
params?: Promise<SegmentParams>
}
// =============
// Utility types
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
function checkFields<_ extends { [k in keyof any]: never }>() {}
// https://github.com/sindresorhus/type-fest
type Numeric = number | bigint
type Zero = 0 | 0n
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'

141
.next/types/cache-life.d.ts vendored Normal file
View File

@ -0,0 +1,141 @@
// Type definitions for Next.js cacheLife configs
declare module 'next/cache' {
export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache'
export {
revalidateTag,
revalidatePath,
unstable_expireTag,
unstable_expirePath,
} from 'next/dist/server/web/spec-extension/revalidate'
export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store'
/**
* Cache this `"use cache"` for a timespan defined by the `"default"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 900 seconds (15 minutes)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 15 minutes, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "default"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"seconds"` profile.
* ```
* stale: 30 seconds
* revalidate: 1 seconds
* expire: 60 seconds (1 minute)
* ```
*
* This cache may be stale on clients for 30 seconds before checking with the server.
* If the server receives a new request after 1 seconds, start revalidating new values in the background.
* If this entry has no traffic for 1 minute it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "seconds"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"minutes"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 60 seconds (1 minute)
* expire: 3600 seconds (1 hour)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 minute, start revalidating new values in the background.
* If this entry has no traffic for 1 hour it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "minutes"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"hours"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 3600 seconds (1 hour)
* expire: 86400 seconds (1 day)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 hour, start revalidating new values in the background.
* If this entry has no traffic for 1 day it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "hours"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"days"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 86400 seconds (1 day)
* expire: 604800 seconds (1 week)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 day, start revalidating new values in the background.
* If this entry has no traffic for 1 week it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "days"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"weeks"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 604800 seconds (1 week)
* expire: 2592000 seconds (30 days)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 week, start revalidating new values in the background.
* If this entry has no traffic for 30 days it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "weeks"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"max"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 2592000 seconds (30 days)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 30 days, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "max"): void
/**
* Cache this `"use cache"` using a custom timespan.
* ```
* stale: ... // seconds
* revalidate: ... // seconds
* expire: ... // seconds
* ```
*
* This is similar to Cache-Control: max-age=`stale`,s-max-age=`revalidate`,stale-while-revalidate=`expire-revalidate`
*
* If a value is left out, the lowest of other cacheLife() calls or the default, is used instead.
*/
export function unstable_cacheLife(profile: {
/**
* This cache may be stale on clients for ... seconds before checking with the server.
*/
stale?: number,
/**
* If the server receives a new request after ... seconds, start revalidating new values in the background.
*/
revalidate?: number,
/**
* If this entry has no traffic for ... seconds it will expire. The next request will recompute it.
*/
expire?: number
}): void
export { cacheTag as unstable_cacheTag } from 'next/dist/server/use-cache/cache-tag'
}

1
.next/types/package.json Normal file
View File

@ -0,0 +1 @@
{"type": "module"}

60
.next/types/routes.d.ts vendored Normal file
View File

@ -0,0 +1,60 @@
// This file is generated automatically by Next.js
// Do not edit this file manually
type AppRoutes = "/" | "/dashboard" | "/login" | "/register"
type PageRoutes = never
type LayoutRoutes = "/"
type RedirectRoutes = never
type RewriteRoutes = never
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes
interface ParamMap {
"/": {}
"/dashboard": {}
"/login": {}
"/register": {}
}
export type ParamsOf<Route extends Routes> = ParamMap[Route]
interface LayoutSlotMap {
"/": never
}
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap }
declare global {
/**
* Props for Next.js App Router page components
* @example
* ```tsx
* export default function Page(props: PageProps<'/blog/[slug]'>) {
* const { slug } = await props.params
* return <div>Blog post: {slug}</div>
* }
* ```
*/
interface PageProps<AppRoute extends AppRoutes> {
params: Promise<ParamMap[AppRoute]>
searchParams: Promise<Record<string, string | string[] | undefined>>
}
/**
* Props for Next.js App Router layout components
* @example
* ```tsx
* export default function Layout(props: LayoutProps<'/dashboard'>) {
* return <div>{props.children}</div>
* }
* ```
*/
type LayoutProps<LayoutRoute extends LayoutRoutes> = {
params: Promise<ParamMap[LayoutRoute]>
children: React.ReactNode
} & {
[K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
}
}

88
.next/types/validator.ts Normal file
View File

@ -0,0 +1,88 @@
// This file is generated automatically by Next.js
// Do not edit this file manually
// This file validates that all pages and layouts export the correct types
import type { AppRoutes, LayoutRoutes, ParamMap } from "./routes.js"
import type { ResolvingMetadata, ResolvingViewport } from "next/types.js"
type AppPageConfig<Route extends AppRoutes = AppRoutes> = {
default: React.ComponentType<{ params: Promise<ParamMap[Route]> } & any> | ((props: { params: Promise<ParamMap[Route]> } & any) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>)
generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[]
generateMetadata?: (
props: { params: Promise<ParamMap[Route]> } & any,
parent: ResolvingMetadata
) => Promise<any> | any
generateViewport?: (
props: { params: Promise<ParamMap[Route]> } & any,
parent: ResolvingViewport
) => Promise<any> | any
metadata?: any
viewport?: any
}
type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
default: React.ComponentType<LayoutProps<Route>> | ((props: LayoutProps<Route>) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>)
generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[]
generateMetadata?: (
props: { params: Promise<ParamMap[Route]> } & any,
parent: ResolvingMetadata
) => Promise<any> | any
generateViewport?: (
props: { params: Promise<ParamMap[Route]> } & any,
parent: ResolvingViewport
) => Promise<any> | any
metadata?: any
viewport?: any
}
// Validate ../../app/dashboard/page.tsx
{
type __IsExpected<Specific extends AppPageConfig<"/dashboard">> = Specific
const handler = {} as typeof import("../../app/dashboard/page.js")
type __Check = __IsExpected<typeof handler>
// @ts-ignore
type __Unused = __Check
}
// Validate ../../app/login/page.tsx
{
type __IsExpected<Specific extends AppPageConfig<"/login">> = Specific
const handler = {} as typeof import("../../app/login/page.js")
type __Check = __IsExpected<typeof handler>
// @ts-ignore
type __Unused = __Check
}
// Validate ../../app/page.tsx
{
type __IsExpected<Specific extends AppPageConfig<"/">> = Specific
const handler = {} as typeof import("../../app/page.js")
type __Check = __IsExpected<typeof handler>
// @ts-ignore
type __Unused = __Check
}
// Validate ../../app/register/page.tsx
{
type __IsExpected<Specific extends AppPageConfig<"/register">> = Specific
const handler = {} as typeof import("../../app/register/page.js")
type __Check = __IsExpected<typeof handler>
// @ts-ignore
type __Unused = __Check
}
// Validate ../../app/layout.tsx
{
type __IsExpected<Specific extends LayoutConfig<"/">> = Specific
const handler = {} as typeof import("../../app/layout.js")
type __Check = __IsExpected<typeof handler>
// @ts-ignore
type __Unused = __Check
}

18
AGENTS.md Normal file
View File

@ -0,0 +1,18 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
These instructions are for AI assistants working in this project.
Always open `@/openspec/AGENTS.md` when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
- Sounds ambiguous and you need the authoritative spec before coding
Use `@/openspec/AGENTS.md` to learn:
- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines
Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->

18
CLAUDE.md Normal file
View File

@ -0,0 +1,18 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
These instructions are for AI assistants working in this project.
Always open `@/openspec/AGENTS.md` when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
- Sounds ambiguous and you need the authoritative spec before coding
Use `@/openspec/AGENTS.md` to learn:
- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines
Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->

5
Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM nginx:alpine AS prod
COPY ./dist/hosting-frontend/browser /usr/share/nginx/html
COPY ./deploy/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

255
README.md
View File

@ -1,59 +1,260 @@
# HostingFrontend
# Portfolio Hosting Platform - Frontend
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.0.
A modern Next.js application for managing and deploying portfolio websites with custom domains.
## Development server
## Tech Stack
To start a local development server, run:
- **Framework**: Next.js 15 with App Router
- **Language**: TypeScript 5.8
- **Styling**: Tailwind CSS 4.1 with shadcn/ui components
- **State Management**: React Context API + hooks
- **Forms**: React Hook Form
- **Icons**: Lucide React
- **Authentication**: JWT Bearer tokens
```bash
ng serve
## Features
- **User Authentication**: Login and registration with JWT tokens
- **Portfolio Management**: Create, upload, and deploy portfolio websites
- **File Upload**: ZIP file upload with progress tracking
- **Dashboard**: Comprehensive portfolio management interface
- **Responsive Design**: Mobile-first, fully responsive UI
- **Protected Routes**: Middleware-based authentication
## Project Structure
```
/
├── app/ # Next.js App Router pages
│ ├── layout.tsx # Root layout with AuthProvider
│ ├── page.tsx # Landing page
│ ├── login/ # Login page
│ ├── register/ # Registration page
│ └── dashboard/ # Dashboard (protected)
├── components/ # React components
│ ├── ui/ # shadcn/ui primitives
│ └── auth/ # Authentication components
├── lib/ # Utilities and API client
│ ├── api-client.ts # Fetch wrapper with auth
│ ├── types.ts # TypeScript interfaces
│ └── utils.ts # Utility functions
├── hooks/ # Custom React hooks
│ ├── use-auth.ts # Authentication hook
│ └── use-portfolios.ts # Portfolio data hook
└── middleware.ts # Route protection
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Getting Started
## Code scaffolding
### Prerequisites
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
- Node.js 18+ and npm
- Backend API running (see API section)
### Installation
```bash
ng generate component component-name
# Install dependencies
npm install
# Set up environment variables
cp .env.local.example .env.local
# Edit .env.local with your API URL
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
### Environment Variables
Create a `.env.local` file:
```env
NEXT_PUBLIC_API_URL=http://localhost:8000/api
```
For production, create `.env.production`:
```env
NEXT_PUBLIC_API_URL=https://api.portfolio-host.com/api
```
### Development
```bash
ng generate --help
# Start development server
npm run dev
```
## Building
Visit [http://localhost:3000](http://localhost:3000)
To build the project run:
### Build
```bash
ng build
# Build for production
npm run build
# Start production server
npm start
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
### Docker Deployment
```bash
ng test
# Build Docker image
docker build -t hosting-frontend .
# Run container
docker run -p 3000:3000 hosting-frontend
```
## Running end-to-end tests
## API Integration
For end-to-end (e2e) testing, run:
The application integrates with a backend API for:
### Authentication Endpoints
- `POST /auth/register` - User registration
- `POST /auth/login` - User login
- `POST /auth/logout` - Logout
- `GET /auth/user` - Get current user
- `POST /auth/refresh` - Refresh token
### Portfolio Endpoints
- `GET /portfolios` - List user's portfolios
- `POST /portfolios` - Create new portfolio
- `POST /portfolios/{id}/upload` - Upload ZIP file
- `POST /portfolios/{id}/deploy` - Deploy portfolio
- `GET /portfolio/random` - Get random portfolio
### API Response Format
All API responses follow this structure:
```typescript
{
success: boolean;
data?: T;
message?: string;
errors?: any;
}
```
## Key Components
### Authentication Flow
1. User logs in via `/login`
2. API returns JWT token
3. Token stored in localStorage
4. AuthContext provides auth state globally
5. Middleware protects `/dashboard/*` routes
6. API client automatically injects Bearer token
### Portfolio Management Flow
1. User creates portfolio (name + domain)
2. Upload ZIP file containing portfolio files
3. Deploy portfolio to make it live
4. Status indicators show portfolio state:
- **Pending Payment**: Not yet active
- **Pending Upload**: Active but no files
- **Uploaded**: Ready to deploy
## Code Style & Conventions
### File Naming
- Components: `PascalCase.tsx`
- Utilities: `kebab-case.ts`
- Pages: Next.js conventions (`page.tsx`, `layout.tsx`)
### Component Patterns
- Use `'use client'` directive for client components
- Server components by default
- Colocate styles with components
### State Management
- Global auth state: `AuthContext`
- Component state: `useState`
- Form state: React Hook Form
### API Calls
- Use `apiClient` from `lib/api-client.ts`
- Wrap in try/catch
- Handle loading and error states
## Migration from Angular
This project was migrated from Angular 20 to Next.js 15. Key changes:
- **Framework**: Angular → Next.js + React
- **Routing**: Angular Router → Next.js App Router
- **State**: RxJS → React Context API
- **Forms**: Angular Reactive Forms → React Hook Form
- **HTTP**: Angular HttpClient → Fetch API
The Angular code has been backed up to `/angular-backup` for reference.
## Deployment
### Production Build
The production build uses Next.js standalone output for optimal performance:
```bash
ng e2e
npm run build
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
Output is in `.next/standalone/`
## Additional Resources
### Environment Configuration
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
- Development: `.env.local`
- Production: `.env.production`
- Staging: `.env.staging`
### Docker
The included `Dockerfile` creates an optimized production image:
```dockerfile
# Build and run
docker build -t portfolio-frontend .
docker run -p 3000:3000 -e NEXT_PUBLIC_API_URL=https://api.example.com portfolio-frontend
```
## Troubleshooting
### Build Errors
**Issue**: Tailwind CSS errors
```bash
# Ensure @tailwindcss/postcss is installed
npm install @tailwindcss/postcss
```
**Issue**: TypeScript errors
```bash
# Regenerate types
rm -rf .next
npm run build
```
### Runtime Errors
**Issue**: API connection failed
- Check `NEXT_PUBLIC_API_URL` in `.env.local`
- Verify backend API is running
- Check CORS configuration on backend
**Issue**: Authentication not working
- Check localStorage for `auth_token`
- Verify JWT token format
- Check middleware configuration
## Contributing
1. Create feature branch from `main`
2. Make changes following code style
3. Test locally with `npm run build`
4. Submit pull request
## License
Copyright © 2025 Portfolio Host. All rights reserved.

View File

@ -0,0 +1,92 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"hosting-frontend": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "16kB",
"maximumError": "32kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "hosting-frontend:build:production"
},
"development": {
"buildTarget": "hosting-frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
}
}
}
}
}
}

View File

@ -0,0 +1,12 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes)
]
};

View File

@ -0,0 +1 @@
@import "tailwindcss";

View File

@ -0,0 +1 @@
<router-outlet />

View File

@ -0,0 +1,23 @@
import {Routes} from '@angular/router';
import {LoginComponent} from './components/login/login.component';
import {RegisterComponent} from './components/register/register.component';
import {LandingComponent} from './components/landing/landing.component';
import {DashboardComponent} from './components/dashboard/dashboard.component';
export const routes: Routes = [
{
path: 'login', component: LoginComponent, title: 'Login - Your App Name'
},
{
path: 'register', component: RegisterComponent, title: 'Register - Your App Name'
},
{
path: '', component: LandingComponent, title: 'Home'
},
{path: 'dashboard', component: DashboardComponent}
];

View File

@ -0,0 +1,23 @@
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, hosting-frontend');
});
});

View File

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
protected title = 'hosting-frontend';
}

Some files were not shown because too many files have changed in this diff Show More