From 7e71b20e8b35992c10bc5b9b3e41e13e1c6da56c Mon Sep 17 00:00:00 2001 From: Vijayabaskar Date: Thu, 5 Jun 2025 08:43:55 +0530 Subject: [PATCH 1/9] initial commit --- apps/cli/src/types.ts | 1 + apps/cli/src/validation.ts | 18 +- .../templates/frontend/angular/.editorconfig | 17 + .../cli/templates/frontend/angular/.gitignore | 42 +++ .../frontend/angular/.postcssrc.json | 5 + .../templates/frontend/angular/angular.json | 106 ++++++ .../templates/frontend/angular/package.json | 48 +++ .../frontend/angular/public/favicon.ico | Bin 0 -> 15086 bytes .../angular/src/app/app.component.css | 0 .../angular/src/app/app.component.html | 336 ++++++++++++++++++ .../angular/src/app/app.component.spec.ts | 29 ++ .../frontend/angular/src/app/app.component.ts | 32 ++ .../frontend/angular/src/app/app.config.ts | 44 +++ .../frontend/angular/src/app/app.routes.ts | 22 ++ .../components/ai-chat/ai-chat.component.ts | 77 ++++ .../components/auth/login/login.component.ts | 104 ++++++ .../auth/signup/signup.component.ts | 125 +++++++ .../dashboard/dashboard.component.ts | 48 +++ .../components/example/example.component.ts | 24 ++ .../src/components/header/header.component.ts | 169 +++++++++ .../src/components/home/home.component.ts | 95 +++++ .../todo-list/todo-list.component.ts | 160 +++++++++ .../angular/src/environments/enviroments.ts | 16 + .../src/environments/environment.prod.ts | 5 + .../frontend/angular/src/guards/auth.guard.ts | 15 + .../templates/frontend/angular/src/index.html | 13 + .../templates/frontend/angular/src/main.ts | 28 ++ .../frontend/angular/src/models/todo.model.ts | 5 + .../angular/src/models/validation.model.ts | 27 ++ .../frontend/angular/src/polyfills.ts | 52 +++ .../angular/src/services/auth.service.ts | 78 ++++ .../angular/src/services/orpc.service.ts | 45 +++ .../angular/src/services/theme.service.ts | 48 +++ .../angular/src/services/todo.service.ts | 53 +++ .../angular/src/services/trpc.service.ts | 39 ++ .../templates/frontend/angular/src/styles.css | 125 +++++++ .../frontend/angular/src/types/env.d.ts | 11 + .../frontend/angular/tsconfig.app.json | 33 ++ .../templates/frontend/angular/tsconfig.json | 46 +++ .../frontend/angular/tsconfig.spec.json | 18 + bun.lock | 2 +- 41 files changed, 2155 insertions(+), 6 deletions(-) create mode 100644 apps/cli/templates/frontend/angular/.editorconfig create mode 100644 apps/cli/templates/frontend/angular/.gitignore create mode 100644 apps/cli/templates/frontend/angular/.postcssrc.json create mode 100644 apps/cli/templates/frontend/angular/angular.json create mode 100644 apps/cli/templates/frontend/angular/package.json create mode 100644 apps/cli/templates/frontend/angular/public/favicon.ico create mode 100644 apps/cli/templates/frontend/angular/src/app/app.component.css create mode 100644 apps/cli/templates/frontend/angular/src/app/app.component.html create mode 100644 apps/cli/templates/frontend/angular/src/app/app.component.spec.ts create mode 100644 apps/cli/templates/frontend/angular/src/app/app.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/app/app.config.ts create mode 100644 apps/cli/templates/frontend/angular/src/app/app.routes.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/ai-chat/ai-chat.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/auth/login/login.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/auth/signup/signup.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/dashboard/dashboard.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/example/example.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/header/header.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/home/home.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/todo-list/todo-list.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/environments/enviroments.ts create mode 100644 apps/cli/templates/frontend/angular/src/environments/environment.prod.ts create mode 100644 apps/cli/templates/frontend/angular/src/guards/auth.guard.ts create mode 100644 apps/cli/templates/frontend/angular/src/index.html create mode 100644 apps/cli/templates/frontend/angular/src/main.ts create mode 100644 apps/cli/templates/frontend/angular/src/models/todo.model.ts create mode 100644 apps/cli/templates/frontend/angular/src/models/validation.model.ts create mode 100644 apps/cli/templates/frontend/angular/src/polyfills.ts create mode 100644 apps/cli/templates/frontend/angular/src/services/auth.service.ts create mode 100644 apps/cli/templates/frontend/angular/src/services/orpc.service.ts create mode 100644 apps/cli/templates/frontend/angular/src/services/theme.service.ts create mode 100644 apps/cli/templates/frontend/angular/src/services/todo.service.ts create mode 100644 apps/cli/templates/frontend/angular/src/services/trpc.service.ts create mode 100644 apps/cli/templates/frontend/angular/src/styles.css create mode 100644 apps/cli/templates/frontend/angular/src/types/env.d.ts create mode 100644 apps/cli/templates/frontend/angular/tsconfig.app.json create mode 100644 apps/cli/templates/frontend/angular/tsconfig.json create mode 100644 apps/cli/templates/frontend/angular/tsconfig.spec.json diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 687092f81..28eb3c1f8 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -29,6 +29,7 @@ export type Frontend = | "native-unistyles" | "svelte" | "solid" + | "angular" | "none"; export type DatabaseSetup = | "turso" diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index d74dc938c..a2a385aed 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -111,7 +111,8 @@ export function processAndValidateFlags( f === "next" || f === "nuxt" || f === "svelte" || - f === "solid", + f === "solid" || + f === "angular", ); const nativeFrontends = validOptions.filter( (f) => f === "native-nativewind" || f === "native-unistyles", @@ -189,7 +190,7 @@ export function processAndValidateFlags( if (providedFlags.has("frontend") && options.frontend) { const incompatibleFrontends = options.frontend.filter( - (f) => f === "nuxt" || f === "solid", + (f) => f === "nuxt" || f === "solid" || f === "angular", ); if (incompatibleFrontends.length > 0) { consola.fatal( @@ -398,7 +399,7 @@ export function validateConfigCompatibility( const includesNuxt = effectiveFrontend?.includes("nuxt"); const includesSvelte = effectiveFrontend?.includes("svelte"); const includesSolid = effectiveFrontend?.includes("solid"); - + const includesAngular = effectiveFrontend?.includes("angular"); if ( (includesNuxt || includesSvelte || includesSolid) && effectiveApi === "trpc" @@ -423,14 +424,15 @@ export function validateConfigCompatibility( f === "tanstack-router" || f === "react-router" || f === "solid" || - f === "next"; + f === "next" const isTauriCompatible = f === "tanstack-router" || f === "react-router" || f === "nuxt" || f === "svelte" || f === "solid" || - f === "next"; + f === "next" || + f === "angular"; if (config.addons?.includes("pwa") && config.addons?.includes("tauri")) { return isPwaCompatible && isTauriCompatible; @@ -498,5 +500,11 @@ export function validateConfigCompatibility( ); process.exit(1); } + if (config.examples.includes("ai") && includesAngular) { + consola.fatal( + "The 'ai' example is not compatible with the Angular frontend.", + ); + process.exit(1); + } } } diff --git a/apps/cli/templates/frontend/angular/.editorconfig b/apps/cli/templates/frontend/angular/.editorconfig new file mode 100644 index 000000000..f166060da --- /dev/null +++ b/apps/cli/templates/frontend/angular/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/apps/cli/templates/frontend/angular/.gitignore b/apps/cli/templates/frontend/angular/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/apps/cli/templates/frontend/angular/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/apps/cli/templates/frontend/angular/.postcssrc.json b/apps/cli/templates/frontend/angular/.postcssrc.json new file mode 100644 index 000000000..85a175673 --- /dev/null +++ b/apps/cli/templates/frontend/angular/.postcssrc.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} diff --git a/apps/cli/templates/frontend/angular/angular.json b/apps/cli/templates/frontend/angular/angular.json new file mode 100644 index 000000000..22f1d0616 --- /dev/null +++ b/apps/cli/templates/frontend/angular/angular.json @@ -0,0 +1,106 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "my-app": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/my-app", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["src/polyfills.ts"], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [], + "allowedCommonJsDependencies": [ + "@trpc/server", + "@trpc/client" + ], + "preserveSymlinks": true + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "sourceMap": true, + "namedChunks": false, + "extractLicenses": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "my-app:build:production" + }, + "development": { + "buildTarget": "my-app:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["src/polyfills.ts"], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/apps/cli/templates/frontend/angular/package.json b/apps/cli/templates/frontend/angular/package.json new file mode 100644 index 000000000..a237eeb0a --- /dev/null +++ b/apps/cli/templates/frontend/angular/package.json @@ -0,0 +1,48 @@ +{ + "name": "web", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "dev": "ng serve --port=3001", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^20.0.0", + "@angular/common": "^20.0.0", + "@angular/compiler": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/forms": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@angular/platform-browser-dynamic": "^20.0.0", + "@angular/router": "^20.0.0", + "@orpc/client": "^1.4.0", + "@orpc/server": "^1.4.0", + "@tailwindcss/postcss": "^4.1.8", + "@tanstack/angular-form": "^1.12.1", + "@tanstack/angular-query-experimental": "^5.80.2", + "better-auth": "^1.2.8", + "ngx-sonner": "^3.1.0", + "postcss": "^8.5.4", + "rxjs": "~7.8.2", + "tailwindcss": "^4.1.8", + "tslib": "^2.8.1", + "zone.js": "~0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^20.0.0", + "@angular/cli": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@types/jasmine": "~5.1.8", + "jasmine-core": "~5.6.0", + "karma": "~6.4.4", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.1", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.7.3" + } +} diff --git a/apps/cli/templates/frontend/angular/public/favicon.ico b/apps/cli/templates/frontend/angular/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 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 Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/apps/cli/templates/frontend/angular/src/app/app.component.css b/apps/cli/templates/frontend/angular/src/app/app.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/apps/cli/templates/frontend/angular/src/app/app.component.html b/apps/cli/templates/frontend/angular/src/app/app.component.html new file mode 100644 index 000000000..36093e187 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app/app.component.html @@ -0,0 +1,336 @@ + + + + + + + + + + + +
+
+
+ +

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/apps/cli/templates/frontend/angular/src/app/app.component.spec.ts b/apps/cli/templates/frontend/angular/src/app/app.component.spec.ts new file mode 100644 index 000000000..5119ea23a --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app/app.component.spec.ts @@ -0,0 +1,29 @@ +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have the 'my-app' title`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('my-app'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, my-app'); + }); +}); diff --git a/apps/cli/templates/frontend/angular/src/app/app.component.ts b/apps/cli/templates/frontend/angular/src/app/app.component.ts new file mode 100644 index 000000000..a344f20d7 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app/app.component.ts @@ -0,0 +1,32 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { NgxSonnerToaster } from 'ngx-sonner'; +import { HeaderComponent } from '../components/header/header.component'; +import { ThemeService } from '../services/theme.service'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [ + CommonModule, + RouterOutlet, + HeaderComponent, + NgxSonnerToaster + ], + template: ` +
+ +
+ +
+ +
+ `, +}) +export class AppComponent { + private themeService = inject(ThemeService); + constructor() { + this.themeService.initTheme(); + } +} diff --git a/apps/cli/templates/frontend/angular/src/app/app.config.ts b/apps/cli/templates/frontend/angular/src/app/app.config.ts new file mode 100644 index 000000000..13479579f --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app/app.config.ts @@ -0,0 +1,44 @@ +import { + type ApplicationConfig, + provideZoneChangeDetection, +} from "@angular/core"; +import { provideAnimations } from "@angular/platform-browser/animations"; +import { provideRouter } from "@angular/router"; + +// import { provideTrpcClient } from "./utils/trpc-client"; +import { provideHttpClient, withInterceptors } from "@angular/common/http"; +import { + QueryClient, + provideTanStackQuery, + withDevtools, +} from "@tanstack/angular-query-experimental"; +import { routes } from "./app.routes"; + +import type { + HttpHandlerFn, + HttpInterceptorFn, + HttpRequest, +} from "@angular/common/http"; + +const withCredentialsInterceptor: HttpInterceptorFn = ( + req: HttpRequest, + next: HttpHandlerFn, +) => { + const modifiedReq = req.clone({ + withCredentials: true, + }); + return next(modifiedReq); +}; +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideAnimations(), + provideHttpClient(withInterceptors([withCredentialsInterceptor])), + // provideTrpcClient(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ loadDevtools: "auto" })), + ), + ], +}; diff --git a/apps/cli/templates/frontend/angular/src/app/app.routes.ts b/apps/cli/templates/frontend/angular/src/app/app.routes.ts new file mode 100644 index 000000000..e6ea12cb1 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app/app.routes.ts @@ -0,0 +1,22 @@ +import type { Route } from '@angular/router'; +import { HomeComponent } from '../components/home/home.component'; +import { AIChatComponent } from '../components/ai-chat/ai-chat.component'; +import { LoginComponent } from '../components/auth/login/login.component'; +import { TodoListComponent } from '../components/todo-list/todo-list.component'; +import { DashboardComponent } from '../components/dashboard/dashboard.component'; +import { SignupComponent } from '../components/auth/signup/signup.component'; +import { authGuard } from '../guards/auth.guard'; + + +export const routes: Route[] = [ + { path: '', component: HomeComponent }, + { path: 'login', component: LoginComponent }, + { path: 'signup', component: SignupComponent }, + { path: 'todos', component: TodoListComponent }, + { + path: 'dashboard', + component: DashboardComponent, + canActivate: [authGuard] + }, + { path: 'ai-chat', component: AIChatComponent } +]; diff --git a/apps/cli/templates/frontend/angular/src/components/ai-chat/ai-chat.component.ts b/apps/cli/templates/frontend/angular/src/components/ai-chat/ai-chat.component.ts new file mode 100644 index 000000000..e6ca6d7d6 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/ai-chat/ai-chat.component.ts @@ -0,0 +1,77 @@ +import { Component, resource, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { environment } from '../../environments/enviroments'; + +@Component({ + selector: 'app-ai-chat', + standalone: true, + imports: [CommonModule, FormsModule], + template: ` +
+
+ +
+
+
+ Ask me anything to get started! +
+
+
+ + +
+
+ + +
+
+
+
+ ` +}) +export class AIChatComponent { + message = ''; + decoder = new TextDecoder(); + characters = resource({ + stream: async () => { + const data = signal<{ value: string; } | { error: unknown; }>({ + value: "", + }); + fetch(environment.baseUrl).then(async (response) => { + if (!response.body) return; + for await (const chunk of response.body) { + const chunkText = this.decoder.decode(chunk); + data.update((prev) => { + if ("value" in prev) { + return { value: `${prev.value} ${chunkText}` }; + } else { + return { error: chunkText }; + } + }); + } + }); + return data; + }, + }); + sendMessage() { + if (this.message.trim()) { + console.log('Sending message:', this.message); + this.message = ''; + } + } +} diff --git a/apps/cli/templates/frontend/angular/src/components/auth/login/login.component.ts b/apps/cli/templates/frontend/angular/src/components/auth/login/login.component.ts new file mode 100644 index 000000000..f06a68cc0 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/auth/login/login.component.ts @@ -0,0 +1,104 @@ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../../services/auth.service'; +import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; +import { loginSchema } from '../../../models/validation.model'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink, TanStackField], + template: ` +
+
+
+

Sign In

+

Welcome back! Please sign in to continue.

+ +
+ +
+ + + + @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { + @for (error of email.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ + +
+ + + @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { + @for (error of password.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+

+ Don't have an account? + + Sign Up + +

+
+
+
+ ` +}) +export class LoginComponent { + email = ''; + password = ''; + private authService = inject(AuthService); + logInForm = injectForm({ + defaultValues: { + email: "", + password: "", + rememberMe: false, + }, + validators: { + onChange: loginSchema, + }, + onSubmit: async (values) => { + this.authService.login(values.value.email, values.value.password); + }, + }); + canSubmit = injectStore(this.logInForm, (state) => state.canSubmit); + isSubmitting = injectStore(this.logInForm, (state) => state.isSubmitting); +} diff --git a/apps/cli/templates/frontend/angular/src/components/auth/signup/signup.component.ts b/apps/cli/templates/frontend/angular/src/components/auth/signup/signup.component.ts new file mode 100644 index 000000000..8dc344b91 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/auth/signup/signup.component.ts @@ -0,0 +1,125 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; +import { signUpSchema } from '../../../models/validation.model'; +import { AuthService } from 'src/services/auth.service'; + +@Component({ + selector: 'app-signup', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink, TanStackField], + template: ` +
+
+
+

Create Account

+

Join us! Create your account to get started.

+ +
+ +
+ + + @if (name.api.state.meta.isDirty && name.api.state.meta.errors) { + @for (error of name.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ + + @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { + @for (error of email.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ + + @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { + @for (error of password.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ +

+ Already have an account? + + Sign In + +

+
+
+
+ ` +}) +export class SignupComponent { + #router = inject(Router); + private authService = inject(AuthService); + signUpForm = injectForm({ + defaultValues: { + email: "", + password: "", + name: "", + }, + validators: { + onChange: signUpSchema, + }, + onSubmit: async (values) => { + this.authService.signUp(values.value.email, values.value.password); + }, + }); + canSubmit = injectStore(this.signUpForm, (state) => state.canSubmit); + isSubmitting = injectStore(this.signUpForm, (state) => state.isSubmitting); +} diff --git a/apps/cli/templates/frontend/angular/src/components/dashboard/dashboard.component.ts b/apps/cli/templates/frontend/angular/src/components/dashboard/dashboard.component.ts new file mode 100644 index 000000000..7427487f3 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/dashboard/dashboard.component.ts @@ -0,0 +1,48 @@ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AuthService } from '../../services/auth.service'; + +@Component({ + selector: 'app-dashboard', + standalone: true, + imports: [CommonModule], + template: ` +
+
+
+
+
+

Dashboard

+

Welcome to your private dashboard

+
+ +
+ +
+
+

Statistics

+

Your activity overview

+
+ +
+

Recent Activity

+

Your latest actions

+
+
+
+
+
+ ` +}) +export class DashboardComponent { + private authService = inject(AuthService); + + logout(): void { + this.authService.logout(); + } +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/angular/src/components/example/example.component.ts b/apps/cli/templates/frontend/angular/src/components/example/example.component.ts new file mode 100644 index 000000000..fe2e2e26f --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/example/example.component.ts @@ -0,0 +1,24 @@ +// import { Component, inject } from '@angular/core'; +// import { useQuery } from '@tanstack/angular-query'; +// import { ORPCService } from '../../services/orpc.service'; + +// @Component({ +// selector: 'app-example', +// template: ` +//
Loading...
+//
Error: {{ query.error }}
+//
+// +// {{ query.data | json }} +//
+// ` +// }) +// export class ExampleComponent { +// private orpcService = inject(ORPCService); +// private client = this.orpcService.getClient(); + +// query = useQuery({ +// queryKey: ['example'], +// queryFn: () => this.client.example.getData.query(), +// }); +// } diff --git a/apps/cli/templates/frontend/angular/src/components/header/header.component.ts b/apps/cli/templates/frontend/angular/src/components/header/header.component.ts new file mode 100644 index 000000000..29cd3b36b --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/header/header.component.ts @@ -0,0 +1,169 @@ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink, RouterLinkActive } from '@angular/router'; +import { ThemeService } from '../../services/theme.service'; +import { AuthService } from '../../services/auth.service'; + +@Component({ + selector: 'app-header', + standalone: true, + imports: [CommonModule, RouterLink, RouterLinkActive], + template: ` +
+
+ + +
+
+ + +
+ + + +
+
+ + + + Sign In + + + + +
+ + +
+
+
{{ this.authService.user()?.name }}
+
{{ this.authService.user()?.email }}
+
+
+ +
+
+
+
+
+
+ `, +}) +export class HeaderComponent { + private themeService = inject(ThemeService); + public authService = inject(AuthService); + + darkMode$ = this.themeService.darkMode$; + isAuthenticated$ = this.authService.isAuthenticated$; + showProfileMenu = false; + showThemeMenu = false; + + get userInitial(): string { + return this.authService.user()?.name?.charAt(0) ?? ''; + } + + toggleThemeMenu(): void { + this.showThemeMenu = !this.showThemeMenu; + if (this.showThemeMenu) { + this.showProfileMenu = false; + } + } + + setTheme(mode: 'light' | 'dark' | 'system'): void { + this.themeService.setTheme(mode); + this.showThemeMenu = false; + } + + toggleProfileMenu(): void { + this.showProfileMenu = !this.showProfileMenu; + if (this.showProfileMenu) { + this.showThemeMenu = false; + } + } + + logout(): void { + this.showProfileMenu = false; + this.authService.logout(); + } +} diff --git a/apps/cli/templates/frontend/angular/src/components/home/home.component.ts b/apps/cli/templates/frontend/angular/src/components/home/home.component.ts new file mode 100644 index 000000000..8514bcd1d --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/home/home.component.ts @@ -0,0 +1,95 @@ +import { Component, inject, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { trpcService } from 'src/services/trpc.service'; +import { injectQuery, QueryClient } from '@tanstack/angular-query-experimental'; + +@Component({ + selector: 'app-home', + standalone: true, + imports: [CommonModule, RouterLink], + template: ` +
+
+
+ ██████╗ ███████╗████████╗████████╗███████╗██████╗
+ ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
+ ██████╔╝█████╗     ██║      ██║   █████╗  ██████╔╝
+ ██╔══██╗██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗
+ ██████╔╝███████╗   ██║      ██║   ███████╗██║  ██║
+ ╚═════╝ ╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝
+
+ ████████╗    ███████╗████████╗ █████╗  ██████╗██╗  ██╗
+ ╚══██╔══╝    ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
+    ██║       ███████╗   ██║   ███████║██║     █████╔╝
+    ██║       ╚════██║   ██║   ██╔══██║██║     ██╔═██╗
+    ██║       ███████║   ██║   ██║  ██║╚██████╗██║  ██╗
+    ╚═╝       ╚══════╝   ╚═╝   ╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝
+ 
+ +
+

API Status

+
+
+ + {{query.isSuccess() ? 'Connected' : 'Disconnected'}} + +
+
+ + +
+

Core Features

+ +
+ +
+

Type-Safe API

+

End-to-end type safety with tRPC

+
+ + +
+

Modern React

+

TanStack Router + TanStack Query

+
+ + +
+

Fast Backend

+

Lightweight Hono server

+
+ + +
+

Beautiful UI

+

TailwindCSS + shadcn/ui components

+
+
+
+ + + +
+
+ `, + styles: [] +}) +export class HomeComponent { + private trpcService = inject(trpcService); + query = injectQuery(() => ({ + queryKey: ['healthCheck'], + queryFn: () => this.trpcService.proxy.healthCheck.query(), + })); + +} diff --git a/apps/cli/templates/frontend/angular/src/components/todo-list/todo-list.component.ts b/apps/cli/templates/frontend/angular/src/components/todo-list/todo-list.component.ts new file mode 100644 index 000000000..9a4fb8c66 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/todo-list/todo-list.component.ts @@ -0,0 +1,160 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; +import { injectMutation, injectQuery, QueryClient } from '@tanstack/angular-query-experimental'; +import { todoSchema } from '../../models/validation.model'; +import { trpcService } from 'src/services/trpc.service'; +@Component({ + selector: 'app-todo-list', + standalone: true, + imports: [CommonModule, FormsModule, TanStackField], + template: ` +
+
+
+

Todo List

+

Manage your tasks efficiently

+ +
+
+ + +
+ + @if (todo.api.state.meta.isDirty && todo.api.state.meta.errors) { + @for (error of todo.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ @if (queryToDo.data()?.length) { + @for (todo of queryToDo.data(); track $index) { +
+ + + + {{ todo.text }} + + + +
+ } + } @else { +
+

No tasks yet. Add your first task above!

+
+ } +
+
+
+ ` +}) +export class TodoListComponent { + private _trpc = inject(trpcService); + queryToDo = injectQuery(() => ({ + queryKey: ["todo"], + queryFn: () => this._trpc.proxy.todo.getAll.query(), + })); + constructor() { + console.log(this.queryToDo.data(), "todos"); + } + mutateToDo = injectMutation(() => ({ + mutationFn: (todo: string) => { + return this._trpc.proxy.todo.create.mutate({ text: todo }); + }, + onSuccess: () => { + this.queryClient.invalidateQueries({ queryKey: ["todo"] }); + this.todoForm.reset(); + }, + })); + updateToDo = injectMutation(() => ({ + mutationFn: (todo: Awaited>[number]) => { + console.log(todo, "todoForm"); + return this._trpc.proxy.todo.toggle.mutate({ + id: todo.id!, + completed: !todo.completed, + }); + }, + onSuccess: () => { + this.queryClient.invalidateQueries({ queryKey: ["todo"] }); + this.todoForm.reset(); + }, + })); + deleteTodo = injectMutation(() => ({ + mutationFn: (id: string) => { + return this._trpc.proxy.todo.delete.mutate({ id: id }); + }, + onSuccess: () => { + this.queryClient.invalidateQueries({ queryKey: ["todo"] }); + }, + })); + + queryClient = inject(QueryClient); + todoForm = injectForm({ + defaultValues: { + todo: "", + }, + validators: { + onChange: todoSchema, + }, + onSubmit: async ({ value }) => { + this.mutateToDo.mutate(value.todo); + }, + }); + canSubmit = injectStore(this.todoForm, (state) => state.canSubmit); + isSubmitting = injectStore(this.todoForm, (state) => state.isSubmitting); + +} diff --git a/apps/cli/templates/frontend/angular/src/environments/enviroments.ts b/apps/cli/templates/frontend/angular/src/environments/enviroments.ts new file mode 100644 index 000000000..e71c41465 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/environments/enviroments.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false, + baseUrl: "http://localhost:3000", +} as const; +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/apps/cli/templates/frontend/angular/src/environments/environment.prod.ts b/apps/cli/templates/frontend/angular/src/environments/environment.prod.ts new file mode 100644 index 000000000..7a5043532 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/environments/environment.prod.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + baseUrl: "http://localhost:6100", + appName: "Auth Kit ", +}; diff --git a/apps/cli/templates/frontend/angular/src/guards/auth.guard.ts b/apps/cli/templates/frontend/angular/src/guards/auth.guard.ts new file mode 100644 index 000000000..f4e668dae --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/guards/auth.guard.ts @@ -0,0 +1,15 @@ +import { inject } from '@angular/core'; +import { Router, type CanActivateFn } from '@angular/router'; +import { AuthService } from '../services/auth.service'; + +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isLoggedIn()) { + return true; + } + + router.navigate(['/login']); + return false; +}; \ No newline at end of file diff --git a/apps/cli/templates/frontend/angular/src/index.html b/apps/cli/templates/frontend/angular/src/index.html new file mode 100644 index 000000000..7fa8cd90c --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + MyApp + + + + + + + + diff --git a/apps/cli/templates/frontend/angular/src/main.ts b/apps/cli/templates/frontend/angular/src/main.ts new file mode 100644 index 000000000..06d187fc0 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/main.ts @@ -0,0 +1,28 @@ +import { enableProdMode } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; +import { environment } from './environments/enviroments'; + +if (environment.production) { + enableProdMode(); + //show this warning only on prod mode + if (window) { + selfXSSWarning(); + } +} +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); + +function selfXSSWarning() { + setTimeout(() => { + console.log( + "%c** STOP **", + "font-weight:bold; font: 2.5em Arial; color: white; background-color: #e11d48; padding-left: 15px; padding-right: 15px; border-radius: 25px; padding-top: 5px; padding-bottom: 5px;", + ); + console.log( + `\n + %cThis is a browser feature intended for developers. Using this console may allow attackers to impersonate you and steal your information sing an attack called Self-XSS. Do not enter or paste code that you do not understand.`, + "font-weight:bold; font: 2em Arial; color: #e11d48;", + ); + }); +} diff --git a/apps/cli/templates/frontend/angular/src/models/todo.model.ts b/apps/cli/templates/frontend/angular/src/models/todo.model.ts new file mode 100644 index 000000000..4d0e04880 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/models/todo.model.ts @@ -0,0 +1,5 @@ +export interface Todo { + id: number; + text: string; + completed: boolean; +} diff --git a/apps/cli/templates/frontend/angular/src/models/validation.model.ts b/apps/cli/templates/frontend/angular/src/models/validation.model.ts new file mode 100644 index 000000000..cd9119e16 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/models/validation.model.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +const signUpSchema = z + .object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.string().email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters") + }); +const loginSchema = z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(1, "Password is required"), + rememberMe: z.boolean(), +}); + +const todoSchema = z.object({ + todo: z.string().nonempty("Todo is required"), +}); +export { + signUpSchema, + loginSchema, + todoSchema, +}; + +type LoginSchema = z.infer; +type SignUpSchema = z.infer; +type TodoSchema = z.infer; +export type { LoginSchema, SignUpSchema, TodoSchema }; diff --git a/apps/cli/templates/frontend/angular/src/polyfills.ts b/apps/cli/templates/frontend/angular/src/polyfills.ts new file mode 100644 index 000000000..ccc203444 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/polyfills.ts @@ -0,0 +1,52 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import "zone.js"; // Included with Angular CLI. + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/apps/cli/templates/frontend/angular/src/services/auth.service.ts b/apps/cli/templates/frontend/angular/src/services/auth.service.ts new file mode 100644 index 000000000..e6594cbdb --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/services/auth.service.ts @@ -0,0 +1,78 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { createAuthClient } from "better-auth/client"; +import type { User } from 'better-auth/types'; +import { toast } from 'ngx-sonner'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private isAuthenticated = new BehaviorSubject(false); + isAuthenticated$ = this.isAuthenticated.asObservable(); + user = signal(null); + authClient = createAuthClient({ + baseURL: 'http://localhost:3000', + }); + router = inject(Router); + constructor() { + this.getSession(); + } + + login(email: string, password: string): void { + this.authClient.signIn.email({ email, password }, { + onSuccess: (session) => { + this.isAuthenticated.next(true); + this.user.set(session.data?.user ?? null); + this.router.navigate(['/dashboard']); + }, + onError: (error) => { + console.error(error); + toast.error(error.error.message); + } + }); + } + signUp(email: string, password: string): void { + this.authClient.signUp.email({ email, password, name: email }, { + onSuccess: (session) => { + this.isAuthenticated.next(true); + this.user.set(session.data?.user ?? null); + this.router.navigate(['/dashboard']); + }, + onError: (error) => { + console.error(error); + toast.error(error.error.message); + } + }); + } + + getSession(): void { + this.authClient.getSession({}, { + onSuccess: (session) => { + this.isAuthenticated.next(true); + this.user.set(session.data?.user ?? null); + }, + onError: (error) => { + console.error(error); + toast.error(error.error.message); + } + }); + } + logout(): void { + this.authClient.signOut({}, { + onSuccess: () => { + this.isAuthenticated.next(false); + this.user.set(null); + this.router.navigate(['/login']); + }, + onError: (error) => { + console.error(error); + toast.error(error.error.message); + } + }); + } + isLoggedIn(): boolean { + return this.isAuthenticated.value; + } +} diff --git a/apps/cli/templates/frontend/angular/src/services/orpc.service.ts b/apps/cli/templates/frontend/angular/src/services/orpc.service.ts new file mode 100644 index 000000000..c5215add7 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/services/orpc.service.ts @@ -0,0 +1,45 @@ +// import { Injectable } from '@angular/core'; +// import { createORPCClient } from "@orpc/client"; +// import { RPCLink } from "@orpc/client/fetch"; +// import { QueryCache, QueryClient } from '@tanstack/angular-query-experimental'; +// import { toast } from 'ngx-sonner'; +// import { appRouter, AppRouter } from '../../../server/src/routers'; +// import { environment } from '../environments/enviroments'; +// import type { RouterClient } from "@orpc/server"; + + +// @Injectable({ +// providedIn: 'root' +// }) +// export class ORPCService { +// private queryClient = new QueryClient({ +// queryCache: new QueryCache({ +// onError: (error) => { +// toast.error(`Error: ${error.message}`, { +// action: { +// label: "retry", +// onClick: () => { +// this.queryClient.invalidateQueries(); +// }, +// }, +// }); +// }, +// }), +// }); + +// private link = new RPCLink({ +// url: `${environment.baseUrl}/rpc`, +// fetch(url, options) { +// return fetch(url, { +// ...options, +// credentials: "include", +// }); +// }, +// }); + +// private client: RouterClient = createORPCClient(this.link); + +// getQueryClient(): QueryClient { +// return this.queryClient; +// } +// } diff --git a/apps/cli/templates/frontend/angular/src/services/theme.service.ts b/apps/cli/templates/frontend/angular/src/services/theme.service.ts new file mode 100644 index 000000000..4bbb4b88a --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/services/theme.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class ThemeService { + private darkMode = new BehaviorSubject(false); + darkMode$ = this.darkMode.asObservable(); + private themeMode = new BehaviorSubject<'light' | 'dark' | 'system'>('system'); + themeMode$ = this.themeMode.asObservable(); + + constructor() { + // Listen for system theme changes + window.matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', e => { + if (this.themeMode.value === 'system') { + this.updateTheme(e.matches); + } + }); + } + + initTheme(): void { + const savedTheme = localStorage.getItem('themeMode') as 'light' | 'dark' | 'system' || 'system'; + this.setTheme(savedTheme); + } + + setTheme(mode: 'light' | 'dark' | 'system'): void { + this.themeMode.next(mode); + localStorage.setItem('themeMode', mode); + + if (mode === 'system') { + const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + this.updateTheme(systemDark); + } else { + this.updateTheme(mode === 'dark'); + } + } + + private updateTheme(isDark: boolean): void { + if (isDark) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + this.darkMode.next(isDark); + } +} diff --git a/apps/cli/templates/frontend/angular/src/services/todo.service.ts b/apps/cli/templates/frontend/angular/src/services/todo.service.ts new file mode 100644 index 000000000..1202230fd --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/services/todo.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import type { Todo } from '../models/todo.model'; + +@Injectable({ + providedIn: 'root' +}) +export class TodoService { + private todos = new BehaviorSubject([ + { id: 1, text: 'Add', completed: true }, + { id: 2, text: 'hgk', completed: false } + ]); + + todos$ = this.todos.asObservable(); + + constructor() { + // Load todos from localStorage if available + const savedTodos = localStorage.getItem('todos'); + if (savedTodos) { + this.todos.next(JSON.parse(savedTodos)); + } + } + + private saveTodos(todos: Todo[]): void { + localStorage.setItem('todos', JSON.stringify(todos)); + this.todos.next(todos); + } + + addTodo(text: string): void { + if (!text.trim()) return; + + const newTodo: Todo = { + id: Date.now(), + text: text.trim(), + completed: false + }; + + const updatedTodos = [...this.todos.value, newTodo]; + this.saveTodos(updatedTodos); + } + + toggleTodo(id: number): void { + const updatedTodos = this.todos.value.map(todo => + todo.id === id ? { ...todo, completed: !todo.completed } : todo + ); + this.saveTodos(updatedTodos); + } + + deleteTodo(id: number): void { + const updatedTodos = this.todos.value.filter(todo => todo.id !== id); + this.saveTodos(updatedTodos); + } +} diff --git a/apps/cli/templates/frontend/angular/src/services/trpc.service.ts b/apps/cli/templates/frontend/angular/src/services/trpc.service.ts new file mode 100644 index 000000000..2ed47d317 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/services/trpc.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from "@angular/core"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import type { AppRouter } from "../../../server/src/routers/index"; + + + +const client = createTRPCClient({ + links: [ + httpBatchLink({ + url: "http://localhost:3000/", + fetch: (url, options) => { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + }), + ], +}); + +@Injectable({ + providedIn: "root", +}) +export class trpcService { + public proxy = createTRPCClient({ + links: [ + httpBatchLink({ + url: "http://localhost:3000/trpc", + fetch: (url, options) => { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + }), + ], + }); + +} diff --git a/apps/cli/templates/frontend/angular/src/styles.css b/apps/cli/templates/frontend/angular/src/styles.css new file mode 100644 index 000000000..b760a742b --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/styles.css @@ -0,0 +1,125 @@ +@import 'tailwindcss'; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --color-primary-50: #f0f9ff; + --color-primary-100: #e0f2fe; + --color-primary-200: #bae6fd; + --color-primary-300: #7dd3fc; + --color-primary-400: #38bdf8; + --color-primary-500: #0ea5e9; + --color-primary-600: #0284c7; + --color-primary-700: #0369a1; + --color-primary-800: #075985; + --color-primary-900: #0c4a6e; + + --color-success-50: #f0fdf4; + --color-success-100: #dcfce7; + --color-success-500: #22c55e; + --color-success-700: #15803d; + + --color-warning-50: #fffbeb; + --color-warning-100: #fef3c7; + --color-warning-500: #f59e0b; + --color-warning-700: #b45309; + + --color-error-50: #fef2f2; + --color-error-100: #fee2e2; + --color-error-500: #ef4444; + --color-error-700: #b91c1c; + + --animate-fade-in: fadeIn 0.3s ease-in-out; + --animate-slide-in: slideIn 0.3s ease-out; + + @keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + @keyframes slideIn { + 0% { + transform: translateY(10px); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } + } +} + +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@utility btn { + @apply px-4 py-2 rounded-md font-medium transition-all duration-200; +} + +@utility btn-primary { + @apply bg-primary-600 text-white hover:bg-primary-700; +} + +@utility btn-secondary { + @apply bg-gray-200 text-gray-800 hover:bg-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600; +} + +@utility input { + @apply bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md px-4 py-2 + focus:outline-hidden focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-600 + focus:border-transparent transition-all duration-200; +} + +@utility card { + @apply bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden transition-all duration-200; +} + +@utility todo-item { + @apply flex items-center gap-3 p-4 border-b border-gray-200 dark:border-gray-700 last:border-0 + transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-700; +} + +@utility icon-btn { + @apply p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-all duration-200; +} + +@layer base { + body { + @apply bg-gray-100 text-gray-900 dark:bg-black dark:text-gray-100 min-h-screen; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + } + + h1, h2, h3, h4, h5, h6 { + @apply font-semibold leading-tight; + } + + h1 { + @apply text-2xl; + } + + h2 { + @apply text-xl; + } + + button { + @apply transition-all duration-200; + } +} diff --git a/apps/cli/templates/frontend/angular/src/types/env.d.ts b/apps/cli/templates/frontend/angular/src/types/env.d.ts new file mode 100644 index 000000000..ecf688dac --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/types/env.d.ts @@ -0,0 +1,11 @@ +declare namespace NodeJS { + interface ProcessEnv { + DATABASE_URL: string; + CORS_ORIGIN: string; + [key: string]: string | undefined; + } +} + +declare const process: { + env: NodeJS.ProcessEnv; +}; diff --git a/apps/cli/templates/frontend/angular/tsconfig.app.json b/apps/cli/templates/frontend/angular/tsconfig.app.json new file mode 100644 index 000000000..2b0e76e42 --- /dev/null +++ b/apps/cli/templates/frontend/angular/tsconfig.app.json @@ -0,0 +1,33 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [ + "node" + ], + "paths": { + "@/*": [ + "./src/*" + ], + "@server/*": [ + "../server/*" + ] + } + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts", + "src/**/*.ts" + ], + "exclude": [ + "src/test.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "../server/**/*" + ] +} diff --git a/apps/cli/templates/frontend/angular/tsconfig.json b/apps/cli/templates/frontend/angular/tsconfig.json new file mode 100644 index 000000000..0a16710de --- /dev/null +++ b/apps/cli/templates/frontend/angular/tsconfig.json @@ -0,0 +1,46 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "baseUrl": ".", + "preserveSymlinks": true, + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ], + "types": [ + "node" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts" + ], + "exclude": [ + "../server/**/*", + "../node_modules/**/*", + "../dist/**/*", + "**/*.spec.ts" + ] +} diff --git a/apps/cli/templates/frontend/angular/tsconfig.spec.json b/apps/cli/templates/frontend/angular/tsconfig.spec.json new file mode 100644 index 000000000..6f9d5d711 --- /dev/null +++ b/apps/cli/templates/frontend/angular/tsconfig.spec.json @@ -0,0 +1,18 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/bun.lock b/bun.lock index 6df6b06fd..2d79c5427 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/cli": { "name": "create-better-t-stack", - "version": "2.14.4", + "version": "2.15.2", "bin": { "create-better-t-stack": "dist/index.js", }, From e3d59fdf9cf17b489bb5a6a0c34cc8fc01644128 Mon Sep 17 00:00:00 2001 From: Vijayabaskar Date: Sat, 7 Jun 2025 22:24:00 +0530 Subject: [PATCH 2/9] angular frontend added --- apps/cli/src/constants.ts | 1 + .../project-generation/template-manager.ts | 48 ++++- apps/cli/src/helpers/setup/api-setup.ts | 13 +- apps/cli/src/validation.ts | 7 +- .../pwa/apps/web/angular/ngsw-config.json | 30 +++ .../web/angular/public/icons/icon-128x128.png | Bin 0 -> 2875 bytes .../web/angular/public/icons/icon-144x144.png | Bin 0 -> 3077 bytes .../web/angular/public/icons/icon-152x152.png | Bin 0 -> 3293 bytes .../web/angular/public/icons/icon-192x192.png | Bin 0 -> 4306 bytes .../web/angular/public/icons/icon-384x384.png | Bin 0 -> 11028 bytes .../web/angular/public/icons/icon-512x512.png | Bin 0 -> 16332 bytes .../web/angular/public/icons/icon-72x72.png | Bin 0 -> 1995 bytes .../web/angular/public/icons/icon-96x96.png | Bin 0 -> 2404 bytes .../web/angular/public/manifest.webmanifest | 57 +++++ .../angular/src/services/rpc.service.ts.hbs | 40 ++++ .../angular/src/services/rpc.service.ts.hbs | 38 ++++ .../components/auth/login/login.component.ts | 109 ++++++++++ .../auth/signup/signup.component.ts | 131 ++++++++++++ .../dashboard/dashboard.component.ts | 24 +++ .../auth/web/angular/src/guards/auth.guard.ts | 15 ++ .../web/angular/src/services/auth.service.ts | 78 +++++++ .../todo-list/todo-list.component.ts.hbs | 197 ++++++++++++++++++ .../frontend/angular/angular.json.hbs | 109 ++++++++++ .../templates/frontend/angular/package.json | 3 - .../frontend/angular/src/app.component.html | 7 + .../angular/src/app.component.spec.ts | 29 +++ .../frontend/angular/src/app.component.ts | 24 +++ .../frontend/angular/src/app.config.ts.hbs | 52 +++++ .../frontend/angular/src/app.routes.ts.hbs | 21 ++ .../components/header/header.component.ts.hbs | 160 ++++++++++++++ .../src/components/home/home.component.ts.hbs | 61 ++++++ .../not-found/not-found-component.ts | 27 +++ .../src/{index.html => index.html.hbs} | 6 + 33 files changed, 1276 insertions(+), 11 deletions(-) create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/ngsw-config.json create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-128x128.png create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-144x144.png create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-152x152.png create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-192x192.png create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-384x384.png create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-512x512.png create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-72x72.png create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-96x96.png create mode 100644 apps/cli/templates/addons/pwa/apps/web/angular/public/manifest.webmanifest create mode 100644 apps/cli/templates/api/orpc/web/angular/src/services/rpc.service.ts.hbs create mode 100644 apps/cli/templates/api/trpc/web/angular/src/services/rpc.service.ts.hbs create mode 100644 apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts create mode 100644 apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.ts create mode 100644 apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.ts create mode 100644 apps/cli/templates/auth/web/angular/src/guards/auth.guard.ts create mode 100644 apps/cli/templates/auth/web/angular/src/services/auth.service.ts create mode 100644 apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs create mode 100644 apps/cli/templates/frontend/angular/angular.json.hbs create mode 100644 apps/cli/templates/frontend/angular/src/app.component.html create mode 100644 apps/cli/templates/frontend/angular/src/app.component.spec.ts create mode 100644 apps/cli/templates/frontend/angular/src/app.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/app.config.ts.hbs create mode 100644 apps/cli/templates/frontend/angular/src/app.routes.ts.hbs create mode 100644 apps/cli/templates/frontend/angular/src/components/header/header.component.ts.hbs create mode 100644 apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs create mode 100644 apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts rename apps/cli/templates/frontend/angular/src/{index.html => index.html.hbs} (56%) diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index e1911306a..dfbf20bae 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -45,6 +45,7 @@ export const dependencyVersionMap = { mongoose: "^8.14.0", "vite-plugin-pwa": "^0.21.2", + "@angular/service-worker" : "^19.2.0", "@vite-pwa/assets-generator": "^0.2.6", "@tauri-apps/cli": "^2.4.0", diff --git a/apps/cli/src/helpers/project-generation/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts index 6edaf6b17..d1c44f5df 100644 --- a/apps/cli/src/helpers/project-generation/template-manager.ts +++ b/apps/cli/src/helpers/project-generation/template-manager.ts @@ -4,6 +4,7 @@ import { globby } from "globby"; import { PKG_ROOT } from "../../constants"; import type { ProjectConfig } from "../../types"; import { processTemplate } from "../../utils/template-processor"; +import { addPackageDependency } from "../../utils/add-package-deps"; async function processAndCopyFiles( sourcePattern: string | string[], @@ -70,12 +71,13 @@ export async function setupFrontendTemplates( const hasNuxtWeb = context.frontend.includes("nuxt"); const hasSvelteWeb = context.frontend.includes("svelte"); const hasSolidWeb = context.frontend.includes("solid"); + const hasAngularWeb = context.frontend.includes("angular"); const hasNativeWind = context.frontend.includes("native-nativewind"); const hasUnistyles = context.frontend.includes("native-unistyles"); const _hasNative = hasNativeWind || hasUnistyles; const isConvex = context.backend === "convex"; - if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) { + if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb || hasAngularWeb) { const webAppDir = path.join(projectDir, "apps/web"); await fs.ensureDir(webAppDir); @@ -180,6 +182,23 @@ export async function setupFrontendTemplates( } else { } } + } else if (hasAngularWeb) { + const angularBaseDir = path.join(PKG_ROOT, "templates/frontend/angular"); + if (await fs.pathExists(angularBaseDir)) { + await processAndCopyFiles("**/*", angularBaseDir, webAppDir, context); + } else { + } + + if (!isConvex && context.api === "none") { + const apiWebAngularDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/web/angular`, + ); + if (await fs.pathExists(apiWebAngularDir)) { + await processAndCopyFiles("**/*", apiWebAngularDir, webAppDir, context); + } else { + } + } } } @@ -378,7 +397,7 @@ export async function setupAuthTemplate( const hasNativeWind = context.frontend.includes("native-nativewind"); const hasUnistyles = context.frontend.includes("native-unistyles"); const hasNative = hasNativeWind || hasUnistyles; - + const hasAngularWeb = context.frontend.includes("angular"); if (serverAppDirExists) { const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base"); if (await fs.pathExists(authServerBaseSrc)) { @@ -435,7 +454,7 @@ export async function setupAuthTemplate( } if ( - (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && + (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb || hasAngularWeb) && webAppDirExists ) { if (hasReactWeb) { @@ -503,6 +522,12 @@ export async function setupAuthTemplate( } else { } } + } else if (hasAngularWeb) { + const authWebAngularSrc = path.join(PKG_ROOT, "templates/auth/web/angular"); + if (await fs.pathExists(authWebAngularSrc)) { + await processAndCopyFiles("**/*", authWebAngularSrc, webAppDir, context); + } else { + } } } @@ -570,6 +595,15 @@ export async function setupAddonsTemplate( ) ) { addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/vite"); + } else if (context.frontend.includes("angular")) { + addonSrcDir = path.join( + PKG_ROOT, + "templates/addons/pwa/apps/web/angular", + ); + await addPackageDependency({ + dependencies: ["@angular/service-worker"], + projectDir: webAppDir, + }); } else { continue; } @@ -608,7 +642,7 @@ export async function setupExamplesTemplate( const hasNuxtWeb = context.frontend.includes("nuxt"); const hasSvelteWeb = context.frontend.includes("svelte"); const hasSolidWeb = context.frontend.includes("solid"); - + const hasAngularWeb = context.frontend.includes("angular"); for (const example of context.examples) { if (example === "none") continue; @@ -758,6 +792,12 @@ export async function setupExamplesTemplate( ); } else { } + } else if (hasAngularWeb) { + const exampleWebAngularSrc = path.join(exampleBaseDir, "web/angular"); + if (await fs.pathExists(exampleWebAngularSrc)) { + await processAndCopyFiles("**/*", exampleWebAngularSrc, webAppDir, context); + } else { + } } } diff --git a/apps/cli/src/helpers/setup/api-setup.ts b/apps/cli/src/helpers/setup/api-setup.ts index 058ec0a67..9cd620a34 100644 --- a/apps/cli/src/helpers/setup/api-setup.ts +++ b/apps/cli/src/helpers/setup/api-setup.ts @@ -19,7 +19,7 @@ export async function setupApi(config: ProjectConfig): Promise { const hasNuxtWeb = frontend.includes("nuxt"); const hasSvelteWeb = frontend.includes("svelte"); const hasSolidWeb = frontend.includes("solid"); - + const hasAngularWeb = frontend.includes("angular"); if (!isConvex && api !== "none") { const serverDir = path.join(projectDir, "apps/server"); const serverDirExists = await fs.pathExists(serverDir); @@ -106,6 +106,17 @@ export async function setupApi(config: ProjectConfig): Promise { projectDir: webDir, }); } + } else if (hasAngularWeb) { + if (api === "orpc") { + await addPackageDependency({ + dependencies: [ + "@orpc/tanstack-query", + "@orpc/client", + "@orpc/server", + ], + projectDir: webDir, + }); + } } } diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index a2a385aed..9165d5379 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -424,7 +424,8 @@ export function validateConfigCompatibility( f === "tanstack-router" || f === "react-router" || f === "solid" || - f === "next" + f === "next" || + f === "angular"; const isTauriCompatible = f === "tanstack-router" || f === "react-router" || @@ -450,11 +451,11 @@ export function validateConfigCompatibility( let incompatibleReason = "Selected frontend is not compatible."; if (config.addons.includes("pwa")) { incompatibleReason = - "PWA requires tanstack-router, react-router, next, or solid."; + "PWA requires tanstack-router, react-router, next, angular, or solid."; } if (config.addons.includes("tauri")) { incompatibleReason = - "Tauri requires tanstack-router, react-router, nuxt, svelte, solid, or next."; + "Tauri requires tanstack-router, react-router, nuxt, svelte, solid, next, or angular."; } consola.fatal( `Incompatible addon/frontend combination: ${incompatibleReason}`, diff --git a/apps/cli/templates/addons/pwa/apps/web/angular/ngsw-config.json b/apps/cli/templates/addons/pwa/apps/web/angular/ngsw-config.json new file mode 100644 index 000000000..69edd2878 --- /dev/null +++ b/apps/cli/templates/addons/pwa/apps/web/angular/ngsw-config.json @@ -0,0 +1,30 @@ +{ + "$schema": "./node_modules/@angular/service-worker/config/schema.json", + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": [ + "/favicon.ico", + "/index.csr.html", + "/index.html", + "/manifest.webmanifest", + "/*.css", + "/*.js" + ] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": [ + "/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" + ] + } + } + ] +} diff --git a/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-128x128.png b/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..5a9a2ccdb34a97a06510d04238d8bedd8e063d3a GIT binary patch literal 2875 zcmV-B3&iw^P)C0008_P)t-s0002& z9EbENqopD5@(y0~Ac4&(;F%uw?h$_L3~Rb4@Sh>}?lk4{A#>dpkIgIViW&F%F}(B? zSI#Ko-665>9e3aedGj;V?;nisG~&uE@!BTIsUYpu3W?t$vbrbqDfEpQ^!YHm_Aa;gF0b?~uc09I z!6@*Z9PFwe>h&?n^Do8tFSGY8s`V$5^d*JzB6;ctX7dkLr6Tu{8S%y`?!PDQwIuHI zG}-qu&Gjz3^)0gF9GvO`U-Af9wkP=YGu8Mox6~cC-WHeT8kFq^VeSQ7pd$H{9rwE@ z^R6WGnH}=FCGPPu)!7}c<`|6C27>GXTJ{A?_X0(q9`Ugw@3JB4_A}9$9`~Ie_028v zsUh&KA?~Ce?YSiC@-WNzGRX5SyWSqE_XkL&Ao78}srm!1FA&+8?yi8L-$IsNNZ!*cF`K19I{qap3}Nq#fz+G~(JL z!P+6a-yEdb8KUVJhTtKu(-ot_2%X3WmCFZ_&e$%S$i%~zlzw4hAp7y}?dj;} z;oslf&(FWMvapSBK>PRh^78TU@9*#G&&RT;Pv0?%000M;NklCo2*8x$)93d|qJeXTx^d$M0)Q>! z`}gm+Z3_jCwE9nlIR1gl2~@VekbuzjA)EV~9n0jEwSjjVL?>tH5fm?t5C(&5zrOR?_VJ?7V~TQ{0Iz+3_W zb_+sEP%7frfcpA;EQkuAUq316*s)`^YQym#2}CZyR8GO9!ILIUA^=7JIlrD70Fwf0 zI3@wHK(Qo~L@TuU_3In+)c|w*X&D4+_ij**=C4>G0QIrhM;cvggf(4br>37G5&sHX~uF=B)S^zYw)8g-UR2^NZj^ARvv z%wJ{zx^xMs@(~z8;douJckf;Vqz4P7+qLT(Ks^HZ1%)I)ClCOO1(wQ&g2|I7Z^Zcm z&{fXw0xAs{Fkr+0A($=+5)hxc+sq3-@~&OG1YqMbtbNz6UF(hSq70y>%le-aA%M9! zQY7XF4M2jf*m!lo$dM#45JZT|T>SAP_et{D0L`}mz{oHV(+O^70UtjyEATZp(A5Ae zRt8Yjg@6?70usOYn3!F`rcF41#|{Iqc(EEFcJta;a4guI{8PG^%5`38JD&M>J_WXGQa36$d8eqeqty>YWMGX+X zabqG#M8#V~QmMsB5C3j=YM5Jae^AUhm0)Rya=Bqq( z=ye^iWy=;{P)Y)%qH!Z&dbw}kzI&^50Wb}{?g;?3M#&_BTR@DLLa=WY5`-WIw*n17 z^OvXt%z!ECOHF_wASy5kz>p0amM^ye=wvo3Eqe#B+qX>tFa^GTeTV>{V2MQl6`uQK z&X_Ud_H7}!h=2@oB!0cR`&G6C0W(GhGucKPz@0ASHaeBz7@dLt5*YotwpvBaDlD)wF(FWHW$|-ASBpa5Nm*7V0)!40K(7$ zY+pfKP|g2UaG<^cL0te26cD_5`{P3T`=&s8VMS3y@#4kv0jJ8+(&(gUAvSjG*ki|@ zKY#Y@-Mgb}o;-g2=;5of;7~4ezDY0z2tWY>9=O1nvW(MfIjiL?>}!F!g7V-*kUb&%yArH#n*h9d^XAQ)H*f!U ZUjT_fSx^~10a*Y5002ovPDHLkV1ljCVV(d0 literal 0 HcmV?d00001 diff --git a/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-144x144.png b/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..11702cd7bd67cee3c26172d0b69968b568c210e0 GIT binary patch literal 3077 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 literal 0 HcmV?d00001 diff --git a/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-384x384.png b/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-384x384.png new file mode 100644 index 0000000000000000000000000000000000000000..613ac793e063b1da8d0dc36dcbd1cc0ae620291d GIT binary patch literal 11028 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 literal 0 HcmV?d00001 diff --git a/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-72x72.png b/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-72x72.png new file mode 100644 index 0000000000000000000000000000000000000000..033724e15f5485c4d86af398a68ca293e6116f72 GIT binary patch literal 1995 zcmV;+2Q>JJP)291V+*= z^06lJ@iNwq8T9WTgUAk`+9JF2BZb^3%m;Ym7q0XGMcgOF zo*(n@GuZeu%=Iw5@*;!h3u5#YR+k?7ogMGqEYs})RJtkn_%qPMDD2W6x#A(I$Sm}o zA^G<$pUy4t>@(lADEhM@>G?Ft@+z(JAaC^$NSYe!pBv}r0bck9LaiqJxheViGtbl@ zy}}5d^Cy_k1%~qyQ1%W*(l7GuH0Hbrq4E(+ix>0pA#9i%>7pLz?J&~A37p9UhRiMW z^fTl6FSzm!Tkir^@&Zxx15Nb|N%sguuqOGsDE7%K^7t~w^AA?`3QY9^NuwkBv?uuV zG0F8Uv-A#5_yj_%CHkKs`M)Xk%`NiuGuQbt!SpV^`7ylqF1h$Fukg`I;a1 zlpOZND)p%%^`9X1u_g4sDDk2n@w_GM^fJx(Gt1j0#_}w=_%FED8maawpV}9m^e340 zCzSOkk@O{s+XQy!2W;&KW9tH5?E+f#4^s6BQK%*Rr6TsMBlMRX^t&hWogMPTDe<@_ z@tGX(wIuJjBX;8kfijmgE?a^d*MW27~eIY}!0%!3FTBIQJjv4c+A@Ig2@4+YSr628< z8ttMT>hd((@i5NuF2&pw9<Kg+s;a1{rKNOoa8*@RRaI3rE*=;6@9*Q|U3Qy|k;Vs;a80s;a80s*R0^h=hc4RaI3vEiEf^xaC#=00bjRL_t(o!|l{%P*hPA z!0{_EF$B`*s`{HM-~hB}QNAp^;Xci8 z+_(->TwGk{EjUgHNt^X9W%!s*`}Xa-0n+tr*RIvcye~Gx*U4!SKvJnm2LorB8XFsf zw0G}bl!!>azD~P$3yz7&^y%VC{>rANC>hV43sPXE^FB3cVSfE={Y!i5WKlgv$63T@c1;pWYo2vrh?3j6svI9y!2)?^Jr=Nb`_4jedu z5>i1yK?TL3LVh+j7XjL}W(_#!h*%aDK$)2>!Gho*#z_LS3n428WNrbOL1@)uLHTSh zU%IplAuB7ZIpUDHxy3xlY(J#CXwrn}WOMoQrS|9mOdb4jgU+fdI~ADRDRugEnC}KM5j_8V%GckSliprk%>dg zp^ccLGc^wm);pZ1hc@gDD;JGV6^qPe+k5~@y3RjQDLgm`LH?dsLXd-ecC zSdiOcXcIN8o}yAEhKBZ~hFWMF3p#=wKHQ%gwn|A+B_@Wpq)!6K(UA!q6*@xA3c9-9 zzI{76IkXTp4hVSf=y+i}19|~{`BI;nrK?f0OHN9n#-jjoW2mMx#-y!%?b1ExN}#UtmI8FnW}c;jYoVI&>)9?$#}O6YKB1 zoduQMvL#aQpok22_vg<+diG53q;CG})|FvExg#SZi_&yNM8t2Y9JLw=P#zZK;o(6~ zjIdn4qIdr59vOh@lPp);0QB}P;m8m~xO$YAu_)4o4=TfiM({zynGoTO1vTV_8j=6n zL~Scqo;>;B#EGvjU#{5v?d~0Zr@_pbGZFIoEv>k}`OV!i+z>dJrI!~_AbGxnliu*3 zRPc<@SxZPOy*myky}nb36B;;Q=qzMuIqCGNtq(jwf|L4jLV(QDc6RdQNhm$^gpYSTjcV2; dIpy?!`T;k-MF8k)@eTk0002ovPDHLkV1mgk?e+iw literal 0 HcmV?d00001 diff --git a/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-96x96.png b/apps/cli/templates/addons/pwa/apps/web/angular/public/icons/icon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..3090dc2d8f93429535c667e616e367c010d941ad GIT binary patch literal 2404 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 W@LC!C(#4Aa0000 { + toast.error(error.message, { + action: { + label: "retry", + onClick: () => { + queryClient.invalidateQueries(); + }, + }, + }); + }, + }), +}); + +@Injectable({ + providedIn: 'root' +}) +export class RpcService { + private link = new RPCLink({ + url: `${environment.baseUrl}/rpc`, + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + }); + + private client: RouterClient = createORPCClient(this.link); +} diff --git a/apps/cli/templates/api/trpc/web/angular/src/services/rpc.service.ts.hbs b/apps/cli/templates/api/trpc/web/angular/src/services/rpc.service.ts.hbs new file mode 100644 index 000000000..32f487083 --- /dev/null +++ b/apps/cli/templates/api/trpc/web/angular/src/services/rpc.service.ts.hbs @@ -0,0 +1,38 @@ +import { Injectable } from "@angular/core"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import type { AppRouter } from "../../../server/src/routers/index"; +import { QueryClient, QueryCache } from "@tanstack/angular-query-experimental"; +import { toast } from "ngx-sonner"; +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error) => { + toast.error(error.message, { + action: { + label: "retry", + onClick: () => { + queryClient.invalidateQueries(); + }, + }, + }); + }, + }), +}); + +@Injectable({ + providedIn: "root", +}) +export class RpcService { + public proxy = createTRPCClient({ + links: [ + httpBatchLink({ + url: "http://localhost:3000/trpc", + fetch: (url, options) => { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + }), + ], + }); +} diff --git a/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts b/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts new file mode 100644 index 000000000..b7f3b5091 --- /dev/null +++ b/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts @@ -0,0 +1,109 @@ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; +import { z } from 'zod'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink, TanStackField], + template: ` +
+
+
+

Sign In

+

Welcome back! Please sign in to continue.

+ +
+ +
+ + + + @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { + @for (error of email.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ + +
+ + + @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { + @for (error of password.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+

+ Don't have an account? + + Sign Up + +

+
+
+
+ ` +}) +export class LoginComponent { + email = ''; + password = ''; + private authService = inject(AuthService); + loginSchema = z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(1, "Password is required"), + rememberMe: z.boolean(), + }); + logInForm = injectForm({ + defaultValues: { + email: "", + password: "", + rememberMe: false, + } as z.infer, + validators: { + onChange: this.loginSchema, + }, + onSubmit: async ({value}) => { + this.authService.login(value.email, value.password); + }, + }); + canSubmit = injectStore(this.logInForm, (state) => state.canSubmit); + isSubmitting = injectStore(this.logInForm, (state) => state.isSubmitting); +} diff --git a/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.ts b/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.ts new file mode 100644 index 000000000..39bf9ad16 --- /dev/null +++ b/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.ts @@ -0,0 +1,131 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; +import { AuthService } from '../../../services/auth.service'; +import { z } from 'zod'; + +@Component({ + selector: 'app-signup', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink, TanStackField], + template: ` +
+
+
+

Create Account

+

Join us! Create your account to get started.

+ +
+ +
+ + + @if (name.api.state.meta.isDirty && name.api.state.meta.errors) { + @for (error of name.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ + + @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { + @for (error of email.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ + + @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { + @for (error of password.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ +

+ Already have an account? + + Sign In + +

+
+
+
+ ` +}) +export class SignupComponent { + #router = inject(Router); + private authService = inject(AuthService); + signUpSchema = z + .object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.string().email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters") + }); + signUpForm = injectForm({ + defaultValues: { + email: "", + password: "", + name: "", + } as z.infer, + validators: { + onChange: this.signUpSchema, + }, + onSubmit: async ({value}) => { + this.authService.signUp(value.email, value.password); + }, + }); + canSubmit = injectStore(this.signUpForm, (state) => state.canSubmit); + isSubmitting = injectStore(this.signUpForm, (state) => state.isSubmitting); +} diff --git a/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.ts b/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.ts new file mode 100644 index 000000000..d38829464 --- /dev/null +++ b/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-dashboard', + standalone: true, + imports: [CommonModule], + template: ` +
+
+
+
+
+

Dashboard

+

Welcome to your private dashboard

+
+
+
+
+
+ ` +}) +export class DashboardComponent { +} diff --git a/apps/cli/templates/auth/web/angular/src/guards/auth.guard.ts b/apps/cli/templates/auth/web/angular/src/guards/auth.guard.ts new file mode 100644 index 000000000..63082b00b --- /dev/null +++ b/apps/cli/templates/auth/web/angular/src/guards/auth.guard.ts @@ -0,0 +1,15 @@ +import { inject } from '@angular/core'; +import { Router, type CanActivateFn } from '@angular/router'; +import { AuthService } from '../../../../../frontend/angular/src/services/auth.service'; + +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isLoggedIn()) { + return true; + } + + router.navigate(['/login']); + return false; +}; diff --git a/apps/cli/templates/auth/web/angular/src/services/auth.service.ts b/apps/cli/templates/auth/web/angular/src/services/auth.service.ts new file mode 100644 index 000000000..e6594cbdb --- /dev/null +++ b/apps/cli/templates/auth/web/angular/src/services/auth.service.ts @@ -0,0 +1,78 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { createAuthClient } from "better-auth/client"; +import type { User } from 'better-auth/types'; +import { toast } from 'ngx-sonner'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private isAuthenticated = new BehaviorSubject(false); + isAuthenticated$ = this.isAuthenticated.asObservable(); + user = signal(null); + authClient = createAuthClient({ + baseURL: 'http://localhost:3000', + }); + router = inject(Router); + constructor() { + this.getSession(); + } + + login(email: string, password: string): void { + this.authClient.signIn.email({ email, password }, { + onSuccess: (session) => { + this.isAuthenticated.next(true); + this.user.set(session.data?.user ?? null); + this.router.navigate(['/dashboard']); + }, + onError: (error) => { + console.error(error); + toast.error(error.error.message); + } + }); + } + signUp(email: string, password: string): void { + this.authClient.signUp.email({ email, password, name: email }, { + onSuccess: (session) => { + this.isAuthenticated.next(true); + this.user.set(session.data?.user ?? null); + this.router.navigate(['/dashboard']); + }, + onError: (error) => { + console.error(error); + toast.error(error.error.message); + } + }); + } + + getSession(): void { + this.authClient.getSession({}, { + onSuccess: (session) => { + this.isAuthenticated.next(true); + this.user.set(session.data?.user ?? null); + }, + onError: (error) => { + console.error(error); + toast.error(error.error.message); + } + }); + } + logout(): void { + this.authClient.signOut({}, { + onSuccess: () => { + this.isAuthenticated.next(false); + this.user.set(null); + this.router.navigate(['/login']); + }, + onError: (error) => { + console.error(error); + toast.error(error.error.message); + } + }); + } + isLoggedIn(): boolean { + return this.isAuthenticated.value; + } +} diff --git a/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs b/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs new file mode 100644 index 000000000..81993c402 --- /dev/null +++ b/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs @@ -0,0 +1,197 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; +import { injectMutation, injectQuery, QueryClient } from '@tanstack/angular-query-experimental'; +import { z } from 'zod'; +{{#if (eq api "trpc")}} +import { RpcService } from '../../services/rpc.service'; +{{/if}} +{{#if (eq api "orpc")}} +import { RpcService } from '../../services/rpc.service'; +{{/if}} +@Component({ + selector: 'app-todo-list', + standalone: true, + imports: [CommonModule, FormsModule, TanStackField], + template: ` +
+
+
+

Todo List

+

Manage your tasks efficiently

+ +
+
+ + +
+ + @if (todo.api.state.meta.isDirty && todo.api.state.meta.errors) { + @for (error of todo.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ @if (queryToDo.data()?.length) { + @for (todo of queryToDo.data(); track $index) { +
+ + + + {{ todo.text }} + + + +
+ } + } @else { +
+

No tasks yet. Add your first task above!

+
+ } +
+
+
+ ` +}) +export class TodoListComponent { + queryClient = inject(QueryClient); + {{#if (eq api "trpc")}} + private _rpc = inject(RpcService); + queryToDo = injectQuery(() => ({ + queryKey: ["todo"], + queryFn: () => this._rpc.proxy.todo.getAll.query(), + })); + + mutateToDo = injectMutation(() => ({ + mutationFn: (todo: string) => { + return this._rpc.proxy.todo.create.mutate({ text: todo }); + }, + onSuccess: () => { + this.queryClient.invalidateQueries({ queryKey: ["todo"] }); + this.todoForm.reset(); + }, + })); + + updateToDo = injectMutation(() => ({ + mutationFn: (todo: Awaited>[number]) => { + console.log(todo, "todoForm"); + return this._rpc.proxy.todo.toggle.mutate({ + id: todo.id!, + completed: !todo.completed, + }); + }, + onSuccess: () => { + this.queryClient.invalidateQueries({ queryKey: ["todo"] }); + this.todoForm.reset(); + }, + })); + + deleteTodo = injectMutation(() => ({ + mutationFn: (id: string) => { + return this._rpc.proxy.todo.delete.mutate({ id: id }); + }, + onSuccess: () => { + this.queryClient.invalidateQueries({ queryKey: ["todo"] }); + }, + })); + + {{/if}} + {{#if (eq api "orpc")}} + private _rpc = inject(RpcService); + + queryToDo = injectQuery(() => this._rpc.utils.todo.getAll.queryOptions()) + + mutateToDo = injectMutation(() => this._rpc.utils.todo.create.mutationOptions({ + onSuccess: () => { + this.queryClient.invalidateQueries({ queryKey: this._rpc.utils.todo.getAll.key() }); + this.todoForm.reset(); + }, + })); + + updateToDo = injectMutation(() => this._rpc.utils.todo.toggle.mutationOptions({ + onSuccess: () => { + this.queryClient.invalidateQueries({ queryKey: this._rpc.utils.todo.getAll.key() }); + this.todoForm.reset(); + }, + })); + + deleteTodo = injectMutation(() => this._rpc.utils.todo.delete.mutationOptions({ + onSuccess: () => { + this.queryClient.invalidateQueries({ queryKey: this._rpc.utils.todo.getAll.key() }); + }, + })); + {{/if}} + + todoSchema = z.object({ + todo: z.string().nonempty("Todo is required"), + }); + + todoForm = injectForm({ + defaultValues: { + todo: "", + } as z.infer, + validators: { + onChange: this.todoSchema, + }, + onSubmit: async ({ value }) => { + this.mutateToDo.mutate(value.todo); + }, + }); + canSubmit = injectStore(this.todoForm, (state) => state.canSubmit); + isSubmitting = injectStore(this.todoForm, (state) => state.isSubmitting); + +} diff --git a/apps/cli/templates/frontend/angular/angular.json.hbs b/apps/cli/templates/frontend/angular/angular.json.hbs new file mode 100644 index 000000000..00d6a192d --- /dev/null +++ b/apps/cli/templates/frontend/angular/angular.json.hbs @@ -0,0 +1,109 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "my-app": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/my-app", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["src/polyfills.ts"], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [], + "allowedCommonJsDependencies": [ + "@trpc/server", + "@trpc/client" + ], + "preserveSymlinks": true + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "sourceMap": true, + "namedChunks": false, + "extractLicenses": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + {{#if (includes addons "pwa")}} + "serviceWorker": "ngsw-config.json" + {{/if}} + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "my-app:build:production" + }, + "development": { + "buildTarget": "my-app:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["src/polyfills.ts"], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/apps/cli/templates/frontend/angular/package.json b/apps/cli/templates/frontend/angular/package.json index a237eeb0a..c18aeca4d 100644 --- a/apps/cli/templates/frontend/angular/package.json +++ b/apps/cli/templates/frontend/angular/package.json @@ -19,12 +19,9 @@ "@angular/platform-browser": "^20.0.0", "@angular/platform-browser-dynamic": "^20.0.0", "@angular/router": "^20.0.0", - "@orpc/client": "^1.4.0", - "@orpc/server": "^1.4.0", "@tailwindcss/postcss": "^4.1.8", "@tanstack/angular-form": "^1.12.1", "@tanstack/angular-query-experimental": "^5.80.2", - "better-auth": "^1.2.8", "ngx-sonner": "^3.1.0", "postcss": "^8.5.4", "rxjs": "~7.8.2", diff --git a/apps/cli/templates/frontend/angular/src/app.component.html b/apps/cli/templates/frontend/angular/src/app.component.html new file mode 100644 index 000000000..e0af27e5d --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app.component.html @@ -0,0 +1,7 @@ +
+ +
+ +
+ +
diff --git a/apps/cli/templates/frontend/angular/src/app.component.spec.ts b/apps/cli/templates/frontend/angular/src/app.component.spec.ts new file mode 100644 index 000000000..5119ea23a --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app.component.spec.ts @@ -0,0 +1,29 @@ +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have the 'my-app' title`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('my-app'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, my-app'); + }); +}); diff --git a/apps/cli/templates/frontend/angular/src/app.component.ts b/apps/cli/templates/frontend/angular/src/app.component.ts new file mode 100644 index 000000000..193c42788 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app.component.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { NgxSonnerToaster } from 'ngx-sonner'; +import { HeaderComponent } from './components/header/header.component'; +import { ThemeService } from './services/theme.service'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [ + CommonModule, + RouterOutlet, + HeaderComponent, + NgxSonnerToaster + ], + templateUrl: './app.component.html', +}) +export class AppComponent { + private themeService = inject(ThemeService); + constructor() { + this.themeService.initTheme(); + } +} diff --git a/apps/cli/templates/frontend/angular/src/app.config.ts.hbs b/apps/cli/templates/frontend/angular/src/app.config.ts.hbs new file mode 100644 index 000000000..7b80797b4 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app.config.ts.hbs @@ -0,0 +1,52 @@ +import { + type ApplicationConfig, + provideZoneChangeDetection, +} from "@angular/core"; +import { provideAnimations } from "@angular/platform-browser/animations"; +import { provideRouter } from "@angular/router"; +import { routes } from "./app.routes"; +import { provideHttpClient, withInterceptors } from "@angular/common/http"; +import type { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from "@angular/common/http"; +{{#if (not (eq api "none"))}} +import { queryClient } from "src/services/rpc.service"; +import { + QueryClient, + provideTanStackQuery, + withDevtools, +} from "@tanstack/angular-query-experimental"; +{{/if}} +{{#if (eq addons "pwa")}} +import { provideServiceWorker } from '@angular/service-worker'; +import { isDevMode } from '@angular/core'; +{{/if}} + + +const withCredentialsInterceptor: HttpInterceptorFn = ( + req: HttpRequest, + next: HttpHandlerFn, +) => { + const modifiedReq = req.clone({ + withCredentials: true, + }); + return next(modifiedReq); +}; +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideAnimations(), + provideHttpClient(withInterceptors([withCredentialsInterceptor])), + {{#if (not (eq api "none"))}} + provideTanStackQuery( + queryClient, + withDevtools(() => ({ loadDevtools: "auto" })), + ), + {{/if}} + {{#if (eq addons "pwa")}} + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + registrationStrategy: 'registerWhenStable:30000', + }), + {{/if}} + ], +}; diff --git a/apps/cli/templates/frontend/angular/src/app.routes.ts.hbs b/apps/cli/templates/frontend/angular/src/app.routes.ts.hbs new file mode 100644 index 000000000..876fe3e72 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/app.routes.ts.hbs @@ -0,0 +1,21 @@ +import type { Route } from '@angular/router'; +import { HomeComponent } from './components/home/home.component'; +import { TodoListComponent } from './components/todo-list/todo-list.component'; +import { NotFoundComponent } from './components/not-found/not-found-component'; +{{#if (not (eq auth "none"))}} +import { LoginComponent } from './components/auth/login/login.component'; +import { DashboardComponent } from './components/dashboard/dashboard.component'; +import { SignupComponent } from './components/auth/signup/signup.component'; +import { authGuard } from './guards/auth.guard'; +{{/if}} + +export const routes: Route[] = [ + { path: '', component: HomeComponent }, + { path: 'todos', component: TodoListComponent }, + {{#if (not (eq auth "none"))}} + { path: 'login', component: LoginComponent }, + { path: 'signup', component: SignupComponent }, + { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }, + {{/if}} + { path: '**', component: NotFoundComponent } +]; diff --git a/apps/cli/templates/frontend/angular/src/components/header/header.component.ts.hbs b/apps/cli/templates/frontend/angular/src/components/header/header.component.ts.hbs new file mode 100644 index 000000000..acf02a11d --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/header/header.component.ts.hbs @@ -0,0 +1,160 @@ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink, RouterLinkActive } from '@angular/router'; +import { ThemeService } from '../../services/theme.service'; +import { AuthService } from '../../services/auth.service'; + +@Component({ + selector: 'app-header', + standalone: true, + imports: [CommonModule, RouterLink, RouterLinkActive], + template: ` +
+
+ + +
+
+ + +
+ + + +
+
+ + {{#if (not (eq auth "none"))}} + + + Sign In + + + +
+ + +
+
+
{{ this.authService.user()?.name }}
+
{{ this.authService.user()?.email }}
+
+
+ +
+
+
+ {{/if}} +
+
+
+ `, +}) +export class HeaderComponent { + + private themeService = inject(ThemeService); + darkMode$ = this.themeService.darkMode$; + showProfileMenu = false; + showThemeMenu = false; + {{#if (not (eq auth "none"))}} + public authService = inject(AuthService); + isAuthenticated$ = this.authService.isAuthenticated$; + get userInitial(): string { + return this.authService.user()?.name?.charAt(0) ?? ''; + } + toggleProfileMenu(): void { + this.showProfileMenu = !this.showProfileMenu; + if (this.showProfileMenu) { + this.showThemeMenu = false; + } + } + logout(): void { + this.showProfileMenu = false; + this.authService.logout(); + } + {{/if}} + toggleThemeMenu(): void { + this.showThemeMenu = !this.showThemeMenu; + if (this.showThemeMenu) { + this.showProfileMenu = false; + } + } + setTheme(mode: 'light' | 'dark' | 'system'): void { + this.themeService.setTheme(mode); + this.showThemeMenu = false; + } +} diff --git a/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs b/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs new file mode 100644 index 000000000..f5d7c3712 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs @@ -0,0 +1,61 @@ +import { Component, inject, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { injectQuery } from '@tanstack/angular-query-experimental'; +{{#if (eq api "trpc")}} +import { RpcService } from '../../services/rpc.service'; +{{/if}} +{{#if (eq api "orpc")}} +import { RpcService } from '../../services/rpc.service'; +{{/if}} + +@Component({ + selector: 'app-home', + standalone: true, + imports: [CommonModule, RouterLink], + template: ` +
+
+
+ ██████╗ ███████╗████████╗████████╗███████╗██████╗
+ ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
+ ██████╔╝█████╗     ██║      ██║   █████╗  ██████╔╝
+ ██╔══██╗██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗
+ ██████╔╝███████╗   ██║      ██║   ███████╗██║  ██║
+ ╚═════╝ ╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝
+
+ ████████╗    ███████╗████████╗ █████╗  ██████╗██╗  ██╗
+ ╚══██╔══╝    ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
+    ██║       ███████╗   ██║   ███████║██║     █████╔╝
+    ██║       ╚════██║   ██║   ██╔══██║██║     ██╔═██╗
+    ██║       ███████║   ██║   ██║  ██║╚██████╗██║  ██╗
+    ╚═╝       ╚══════╝   ╚═╝   ╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝
+ 
+ {{#if (not (eq api "none"))}} +
+

API Status

+
+
+ + {{query.isSuccess() ? 'Connected' : 'Disconnected'}} + +
+
+ {{/if}} +
+
+ `, + styles: [] +}) +export class HomeComponent { + {{#if (eq api "trpc")}} + private _rpc = inject(RpcService); + query = injectQuery(() => ({ + queryKey: ['healthCheck'], + queryFn: () => this._rpc.proxy.healthCheck.query(), + })); + {{/if}} + {{#if (eq api "orpc")}} + private _rpc = inject(RpcService); + query = injectQuery(() => this._rpc.utils.healthCheck.queryOptions()); + {{/if}} diff --git a/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts b/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts new file mode 100644 index 000000000..e27dce928 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-not-found', + standalone: true, + imports: [CommonModule, RouterLink], + template: ` +
+
+

404

+

Page Not Found

+

+ The page you're looking for doesn't exist or has been moved. +

+ + Return Home + +
+
+ ` +}) +export class NotFoundComponent {} diff --git a/apps/cli/templates/frontend/angular/src/index.html b/apps/cli/templates/frontend/angular/src/index.html.hbs similarity index 56% rename from apps/cli/templates/frontend/angular/src/index.html rename to apps/cli/templates/frontend/angular/src/index.html.hbs index 7fa8cd90c..329727baa 100644 --- a/apps/cli/templates/frontend/angular/src/index.html +++ b/apps/cli/templates/frontend/angular/src/index.html.hbs @@ -6,8 +6,14 @@ + {{#if addons.includes("pwa")}} + + {{/if}} + {{#if addons.includes("pwa")}} + + {{/if}} From 1094524a0b679b0ab3f38ed118a1a43b1b109d53 Mon Sep 17 00:00:00 2001 From: Vijayabaskar Date: Sat, 7 Jun 2025 23:29:10 +0530 Subject: [PATCH 3/9] angular template clearn up --- apps/cli/src/cli.ts | 1 + apps/cli/src/prompts/frontend.ts | 5 + .../auth/login/login.component.html | 56 +++ .../components/auth/login/login.component.ts | 72 +--- .../auth/signup/signup.component.ts | 94 +---- .../auth/signup/signup.coponent.html | 91 +++++ .../dashboard/dashboard.component.html | 12 + .../dashboard/dashboard.component.ts | 15 +- .../todo-list/todo-list.component.html | 90 +++++ .../todo-list/todo-list.component.ts.hbs | 93 +---- .../angular/src/app.component.spec.ts | 29 -- .../angular/src/app/app.component.html | 343 +----------------- .../frontend/angular/src/app/app.component.ts | 10 +- .../components/ai-chat/ai-chat.component.ts | 77 ---- .../components/auth/login/login.component.ts | 104 ------ .../auth/signup/signup.component.ts | 125 ------- .../dashboard/dashboard.component.ts | 48 --- .../components/example/example.component.ts | 24 -- .../components/header/header.component.html | 113 ++++++ .../src/components/header/header.component.ts | 169 --------- .../components/header/header.component.ts.hbs | 116 +----- .../src/components/home/home.component.html | 30 ++ .../src/components/home/home.component.ts | 95 ----- .../src/components/home/home.component.ts.hbs | 33 +- .../not-found/not-found-component.html | 15 + .../not-found/not-found-component.ts | 18 +- .../todo-list/todo-list.component.ts | 160 -------- .../frontend/angular/src/guards/auth.guard.ts | 15 - .../frontend/angular/src/models/todo.model.ts | 5 - .../angular/src/models/validation.model.ts | 27 -- .../angular/src/services/auth.service.ts | 78 ---- .../angular/src/services/orpc.service.ts | 45 --- .../angular/src/services/todo.service.ts | 53 --- .../angular/src/services/trpc.service.ts | 39 -- 34 files changed, 428 insertions(+), 1872 deletions(-) create mode 100644 apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.html create mode 100644 apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.coponent.html create mode 100644 apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.html create mode 100644 apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.html delete mode 100644 apps/cli/templates/frontend/angular/src/app.component.spec.ts delete mode 100644 apps/cli/templates/frontend/angular/src/components/ai-chat/ai-chat.component.ts delete mode 100644 apps/cli/templates/frontend/angular/src/components/auth/login/login.component.ts delete mode 100644 apps/cli/templates/frontend/angular/src/components/auth/signup/signup.component.ts delete mode 100644 apps/cli/templates/frontend/angular/src/components/dashboard/dashboard.component.ts delete mode 100644 apps/cli/templates/frontend/angular/src/components/example/example.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/header/header.component.html delete mode 100644 apps/cli/templates/frontend/angular/src/components/header/header.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/home/home.component.html delete mode 100644 apps/cli/templates/frontend/angular/src/components/home/home.component.ts create mode 100644 apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.html delete mode 100644 apps/cli/templates/frontend/angular/src/components/todo-list/todo-list.component.ts delete mode 100644 apps/cli/templates/frontend/angular/src/guards/auth.guard.ts delete mode 100644 apps/cli/templates/frontend/angular/src/models/todo.model.ts delete mode 100644 apps/cli/templates/frontend/angular/src/models/validation.model.ts delete mode 100644 apps/cli/templates/frontend/angular/src/services/auth.service.ts delete mode 100644 apps/cli/templates/frontend/angular/src/services/orpc.service.ts delete mode 100644 apps/cli/templates/frontend/angular/src/services/todo.service.ts delete mode 100644 apps/cli/templates/frontend/angular/src/services/trpc.service.ts diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts index c12d381a8..8354c4c15 100644 --- a/apps/cli/src/cli.ts +++ b/apps/cli/src/cli.ts @@ -47,6 +47,7 @@ export async function parseCliArguments(): Promise { "native-nativewind", "native-unistyles", "svelte", + "angular", "solid", "none", ], diff --git a/apps/cli/src/prompts/frontend.ts b/apps/cli/src/prompts/frontend.ts index c846a0720..659ac4996 100644 --- a/apps/cli/src/prompts/frontend.ts +++ b/apps/cli/src/prompts/frontend.ts @@ -71,6 +71,11 @@ export async function getFrontendChoice( label: "TanStack Start (beta)", hint: "SSR, Server Functions, API Routes and more with TanStack Router", }, + { + value: "angular" as const, + label: "Angular", + hint: "The web framework that empowers developers to build fast, reliable applications", + }, ]; const webOptions = allWebOptions.filter((option) => { diff --git a/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.html b/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.html new file mode 100644 index 000000000..c3b7d7114 --- /dev/null +++ b/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.html @@ -0,0 +1,56 @@ +
+
+
+

Sign In

+

Welcome back! Please sign in to continue.

+ +
+ +
+ + + + @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { + @for (error of email.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ + +
+ + + @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { + @for (error of password.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+

+ Don't have an account? + + Sign Up + +

+
+
+
diff --git a/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts b/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts index b7f3b5091..8d862e1ac 100644 --- a/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts +++ b/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts @@ -10,77 +10,7 @@ import { z } from 'zod'; selector: 'app-login', standalone: true, imports: [CommonModule, FormsModule, RouterLink, TanStackField], - template: ` -
-
-
-

Sign In

-

Welcome back! Please sign in to continue.

- -
- -
- - - - @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { - @for (error of email.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- - -
- - - @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { - @for (error of password.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
-

- Don't have an account? - - Sign Up - -

-
-
-
- ` + templateUrl: './login.component.html', }) export class LoginComponent { email = ''; diff --git a/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.ts b/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.ts index 39bf9ad16..59b606ef2 100644 --- a/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.ts +++ b/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.ts @@ -10,99 +10,7 @@ import { z } from 'zod'; selector: 'app-signup', standalone: true, imports: [CommonModule, FormsModule, RouterLink, TanStackField], - template: ` -
-
-
-

Create Account

-

Join us! Create your account to get started.

- -
- -
- - - @if (name.api.state.meta.isDirty && name.api.state.meta.errors) { - @for (error of name.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
- - - @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { - @for (error of email.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
- - - @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { - @for (error of password.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
- -

- Already have an account? - - Sign In - -

-
-
-
- ` + templateUrl : './signup.component.html' }) export class SignupComponent { #router = inject(Router); diff --git a/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.coponent.html b/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.coponent.html new file mode 100644 index 000000000..52e1dd118 --- /dev/null +++ b/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.coponent.html @@ -0,0 +1,91 @@ +
+
+
+

Create Account

+

Join us! Create your account to get started.

+ +
+ +
+ + + @if (name.api.state.meta.isDirty && name.api.state.meta.errors) { + @for (error of name.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ + + @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { + @for (error of email.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ + + @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { + @for (error of password.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ +

+ Already have an account? + + Sign In + +

+
+
+
diff --git a/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.html b/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.html new file mode 100644 index 000000000..4961c31a8 --- /dev/null +++ b/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.html @@ -0,0 +1,12 @@ +
+
+
+
+
+

Dashboard

+

Welcome to your private dashboard

+
+
+
+
+
diff --git a/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.ts b/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.ts index d38829464..d6e7cc911 100644 --- a/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.ts +++ b/apps/cli/templates/auth/web/angular/src/components/dashboard/dashboard.component.ts @@ -5,20 +5,7 @@ import { Component } from '@angular/core'; selector: 'app-dashboard', standalone: true, imports: [CommonModule], - template: ` -
-
-
-
-
-

Dashboard

-

Welcome to your private dashboard

-
-
-
-
-
- ` + templateUrl: './dashboard.component.html' }) export class DashboardComponent { } diff --git a/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.html b/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.html new file mode 100644 index 000000000..fa19442ab --- /dev/null +++ b/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.html @@ -0,0 +1,90 @@ +
+
+
+

Todo List

+

Manage your tasks efficiently

+ +
+
+ + +
+ + @if (todo.api.state.meta.isDirty && todo.api.state.meta.errors) { + @for (error of todo.api.state.meta.errors; track $index) { + {{ error.message }} + } + } +
+
+ +
+ @if (queryToDo.data()?.length) { + @for (todo of queryToDo.data(); track $index) { +
+ + + + {{ todo.text }} + + + +
+ } + } @else { +
+

No tasks yet. Add your first task above!

+
+ } +
+
+
diff --git a/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs b/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs index 81993c402..2086f688a 100644 --- a/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs +++ b/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs @@ -14,98 +14,7 @@ import { RpcService } from '../../services/rpc.service'; selector: 'app-todo-list', standalone: true, imports: [CommonModule, FormsModule, TanStackField], - template: ` -
-
-
-

Todo List

-

Manage your tasks efficiently

- -
-
- - -
- - @if (todo.api.state.meta.isDirty && todo.api.state.meta.errors) { - @for (error of todo.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
- @if (queryToDo.data()?.length) { - @for (todo of queryToDo.data(); track $index) { -
- - - - {{ todo.text }} - - - -
- } - } @else { -
-

No tasks yet. Add your first task above!

-
- } -
-
-
- ` + templateUrl: './todo-list.component.html' }) export class TodoListComponent { queryClient = inject(QueryClient); diff --git a/apps/cli/templates/frontend/angular/src/app.component.spec.ts b/apps/cli/templates/frontend/angular/src/app.component.spec.ts deleted file mode 100644 index 5119ea23a..000000000 --- a/apps/cli/templates/frontend/angular/src/app.component.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AppComponent], - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); - - it(`should have the 'my-app' title`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('my-app'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, my-app'); - }); -}); diff --git a/apps/cli/templates/frontend/angular/src/app/app.component.html b/apps/cli/templates/frontend/angular/src/app/app.component.html index 36093e187..e0af27e5d 100644 --- a/apps/cli/templates/frontend/angular/src/app/app.component.html +++ b/apps/cli/templates/frontend/angular/src/app/app.component.html @@ -1,336 +1,7 @@ - - - - - - - - - - - -
-
-
- -

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/apps/cli/templates/frontend/angular/src/app/app.component.ts b/apps/cli/templates/frontend/angular/src/app/app.component.ts index a344f20d7..2a09fd76d 100644 --- a/apps/cli/templates/frontend/angular/src/app/app.component.ts +++ b/apps/cli/templates/frontend/angular/src/app/app.component.ts @@ -14,15 +14,7 @@ import { ThemeService } from '../services/theme.service'; HeaderComponent, NgxSonnerToaster ], - template: ` -
- -
- -
- -
- `, + templateUrl: './app.component.html', }) export class AppComponent { private themeService = inject(ThemeService); diff --git a/apps/cli/templates/frontend/angular/src/components/ai-chat/ai-chat.component.ts b/apps/cli/templates/frontend/angular/src/components/ai-chat/ai-chat.component.ts deleted file mode 100644 index e6ca6d7d6..000000000 --- a/apps/cli/templates/frontend/angular/src/components/ai-chat/ai-chat.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Component, resource, signal } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { environment } from '../../environments/enviroments'; - -@Component({ - selector: 'app-ai-chat', - standalone: true, - imports: [CommonModule, FormsModule], - template: ` -
-
- -
-
-
- Ask me anything to get started! -
-
-
- - -
-
- - -
-
-
-
- ` -}) -export class AIChatComponent { - message = ''; - decoder = new TextDecoder(); - characters = resource({ - stream: async () => { - const data = signal<{ value: string; } | { error: unknown; }>({ - value: "", - }); - fetch(environment.baseUrl).then(async (response) => { - if (!response.body) return; - for await (const chunk of response.body) { - const chunkText = this.decoder.decode(chunk); - data.update((prev) => { - if ("value" in prev) { - return { value: `${prev.value} ${chunkText}` }; - } else { - return { error: chunkText }; - } - }); - } - }); - return data; - }, - }); - sendMessage() { - if (this.message.trim()) { - console.log('Sending message:', this.message); - this.message = ''; - } - } -} diff --git a/apps/cli/templates/frontend/angular/src/components/auth/login/login.component.ts b/apps/cli/templates/frontend/angular/src/components/auth/login/login.component.ts deleted file mode 100644 index f06a68cc0..000000000 --- a/apps/cli/templates/frontend/angular/src/components/auth/login/login.component.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { RouterLink } from '@angular/router'; -import { AuthService } from '../../../services/auth.service'; -import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; -import { loginSchema } from '../../../models/validation.model'; - -@Component({ - selector: 'app-login', - standalone: true, - imports: [CommonModule, FormsModule, RouterLink, TanStackField], - template: ` -
-
-
-

Sign In

-

Welcome back! Please sign in to continue.

- -
- -
- - - - @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { - @for (error of email.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- - -
- - - @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { - @for (error of password.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
-

- Don't have an account? - - Sign Up - -

-
-
-
- ` -}) -export class LoginComponent { - email = ''; - password = ''; - private authService = inject(AuthService); - logInForm = injectForm({ - defaultValues: { - email: "", - password: "", - rememberMe: false, - }, - validators: { - onChange: loginSchema, - }, - onSubmit: async (values) => { - this.authService.login(values.value.email, values.value.password); - }, - }); - canSubmit = injectStore(this.logInForm, (state) => state.canSubmit); - isSubmitting = injectStore(this.logInForm, (state) => state.isSubmitting); -} diff --git a/apps/cli/templates/frontend/angular/src/components/auth/signup/signup.component.ts b/apps/cli/templates/frontend/angular/src/components/auth/signup/signup.component.ts deleted file mode 100644 index 8dc344b91..000000000 --- a/apps/cli/templates/frontend/angular/src/components/auth/signup/signup.component.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { Router, RouterLink } from '@angular/router'; -import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; -import { signUpSchema } from '../../../models/validation.model'; -import { AuthService } from 'src/services/auth.service'; - -@Component({ - selector: 'app-signup', - standalone: true, - imports: [CommonModule, FormsModule, RouterLink, TanStackField], - template: ` -
-
-
-

Create Account

-

Join us! Create your account to get started.

- -
- -
- - - @if (name.api.state.meta.isDirty && name.api.state.meta.errors) { - @for (error of name.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
- - - @if (email.api.state.meta.isDirty && email.api.state.meta.errors) { - @for (error of email.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
- - - @if (password.api.state.meta.isDirty && password.api.state.meta.errors) { - @for (error of password.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
- -

- Already have an account? - - Sign In - -

-
-
-
- ` -}) -export class SignupComponent { - #router = inject(Router); - private authService = inject(AuthService); - signUpForm = injectForm({ - defaultValues: { - email: "", - password: "", - name: "", - }, - validators: { - onChange: signUpSchema, - }, - onSubmit: async (values) => { - this.authService.signUp(values.value.email, values.value.password); - }, - }); - canSubmit = injectStore(this.signUpForm, (state) => state.canSubmit); - isSubmitting = injectStore(this.signUpForm, (state) => state.isSubmitting); -} diff --git a/apps/cli/templates/frontend/angular/src/components/dashboard/dashboard.component.ts b/apps/cli/templates/frontend/angular/src/components/dashboard/dashboard.component.ts deleted file mode 100644 index 7427487f3..000000000 --- a/apps/cli/templates/frontend/angular/src/components/dashboard/dashboard.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { AuthService } from '../../services/auth.service'; - -@Component({ - selector: 'app-dashboard', - standalone: true, - imports: [CommonModule], - template: ` -
-
-
-
-
-

Dashboard

-

Welcome to your private dashboard

-
- -
- -
-
-

Statistics

-

Your activity overview

-
- -
-

Recent Activity

-

Your latest actions

-
-
-
-
-
- ` -}) -export class DashboardComponent { - private authService = inject(AuthService); - - logout(): void { - this.authService.logout(); - } -} \ No newline at end of file diff --git a/apps/cli/templates/frontend/angular/src/components/example/example.component.ts b/apps/cli/templates/frontend/angular/src/components/example/example.component.ts deleted file mode 100644 index fe2e2e26f..000000000 --- a/apps/cli/templates/frontend/angular/src/components/example/example.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -// import { Component, inject } from '@angular/core'; -// import { useQuery } from '@tanstack/angular-query'; -// import { ORPCService } from '../../services/orpc.service'; - -// @Component({ -// selector: 'app-example', -// template: ` -//
Loading...
-//
Error: {{ query.error }}
-//
-// -// {{ query.data | json }} -//
-// ` -// }) -// export class ExampleComponent { -// private orpcService = inject(ORPCService); -// private client = this.orpcService.getClient(); - -// query = useQuery({ -// queryKey: ['example'], -// queryFn: () => this.client.example.getData.query(), -// }); -// } diff --git a/apps/cli/templates/frontend/angular/src/components/header/header.component.html b/apps/cli/templates/frontend/angular/src/components/header/header.component.html new file mode 100644 index 000000000..cd9013544 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/header/header.component.html @@ -0,0 +1,113 @@ +
+
+ + +
+
+ + +
+ + + +
+
+ + {{#if (not (eq auth "none"))}} + + + Sign In + + + +
+ + +
+
+
{{ this.authService.user()?.name }}
+
{{ this.authService.user()?.email }}
+
+
+ +
+
+
+ {{/if}} +
+
+
diff --git a/apps/cli/templates/frontend/angular/src/components/header/header.component.ts b/apps/cli/templates/frontend/angular/src/components/header/header.component.ts deleted file mode 100644 index 29cd3b36b..000000000 --- a/apps/cli/templates/frontend/angular/src/components/header/header.component.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterLink, RouterLinkActive } from '@angular/router'; -import { ThemeService } from '../../services/theme.service'; -import { AuthService } from '../../services/auth.service'; - -@Component({ - selector: 'app-header', - standalone: true, - imports: [CommonModule, RouterLink, RouterLinkActive], - template: ` -
-
- - -
-
- - -
- - - -
-
- - - - Sign In - - - - -
- - -
-
-
{{ this.authService.user()?.name }}
-
{{ this.authService.user()?.email }}
-
-
- -
-
-
-
-
-
- `, -}) -export class HeaderComponent { - private themeService = inject(ThemeService); - public authService = inject(AuthService); - - darkMode$ = this.themeService.darkMode$; - isAuthenticated$ = this.authService.isAuthenticated$; - showProfileMenu = false; - showThemeMenu = false; - - get userInitial(): string { - return this.authService.user()?.name?.charAt(0) ?? ''; - } - - toggleThemeMenu(): void { - this.showThemeMenu = !this.showThemeMenu; - if (this.showThemeMenu) { - this.showProfileMenu = false; - } - } - - setTheme(mode: 'light' | 'dark' | 'system'): void { - this.themeService.setTheme(mode); - this.showThemeMenu = false; - } - - toggleProfileMenu(): void { - this.showProfileMenu = !this.showProfileMenu; - if (this.showProfileMenu) { - this.showThemeMenu = false; - } - } - - logout(): void { - this.showProfileMenu = false; - this.authService.logout(); - } -} diff --git a/apps/cli/templates/frontend/angular/src/components/header/header.component.ts.hbs b/apps/cli/templates/frontend/angular/src/components/header/header.component.ts.hbs index acf02a11d..a38f17bba 100644 --- a/apps/cli/templates/frontend/angular/src/components/header/header.component.ts.hbs +++ b/apps/cli/templates/frontend/angular/src/components/header/header.component.ts.hbs @@ -8,121 +8,7 @@ import { AuthService } from '../../services/auth.service'; selector: 'app-header', standalone: true, imports: [CommonModule, RouterLink, RouterLinkActive], - template: ` -
-
- - -
-
- - -
- - - -
-
- - {{#if (not (eq auth "none"))}} - - - Sign In - - - -
- - -
-
-
{{ this.authService.user()?.name }}
-
{{ this.authService.user()?.email }}
-
-
- -
-
-
- {{/if}} -
-
-
- `, + templateUrl: './header.component.html' }) export class HeaderComponent { diff --git a/apps/cli/templates/frontend/angular/src/components/home/home.component.html b/apps/cli/templates/frontend/angular/src/components/home/home.component.html new file mode 100644 index 000000000..fbef2b398 --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/home/home.component.html @@ -0,0 +1,30 @@ +
+
+
+██████╗ ███████╗████████╗████████╗███████╗██████╗
+██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
+██████╔╝█████╗     ██║      ██║   █████╗  ██████╔╝
+██╔══██╗██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗
+██████╔╝███████╗   ██║      ██║   ███████╗██║  ██║
+╚═════╝ ╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝
+
+████████╗    ███████╗████████╗ █████╗  ██████╗██╗  ██╗
+╚══██╔══╝    ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
+██║       ███████╗   ██║   ███████║██║     █████╔╝
+██║       ╚════██║   ██║   ██╔══██║██║     ██╔═██╗
+██║       ███████║   ██║   ██║  ██║╚██████╗██║  ██╗
+╚═╝       ╚══════╝   ╚═╝   ╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝
+
+ {{#if (not (eq api "none"))}} +
+

API Status

+
+
+ + {{query.isSuccess() ? 'Connected' : 'Disconnected'}} + +
+
+ {{/if}} +
+
diff --git a/apps/cli/templates/frontend/angular/src/components/home/home.component.ts b/apps/cli/templates/frontend/angular/src/components/home/home.component.ts deleted file mode 100644 index 8514bcd1d..000000000 --- a/apps/cli/templates/frontend/angular/src/components/home/home.component.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Component, inject, signal } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterLink } from '@angular/router'; -import { trpcService } from 'src/services/trpc.service'; -import { injectQuery, QueryClient } from '@tanstack/angular-query-experimental'; - -@Component({ - selector: 'app-home', - standalone: true, - imports: [CommonModule, RouterLink], - template: ` -
-
-
- ██████╗ ███████╗████████╗████████╗███████╗██████╗
- ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
- ██████╔╝█████╗     ██║      ██║   █████╗  ██████╔╝
- ██╔══██╗██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗
- ██████╔╝███████╗   ██║      ██║   ███████╗██║  ██║
- ╚═════╝ ╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝
-
- ████████╗    ███████╗████████╗ █████╗  ██████╗██╗  ██╗
- ╚══██╔══╝    ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
-    ██║       ███████╗   ██║   ███████║██║     █████╔╝
-    ██║       ╚════██║   ██║   ██╔══██║██║     ██╔═██╗
-    ██║       ███████║   ██║   ██║  ██║╚██████╗██║  ██╗
-    ╚═╝       ╚══════╝   ╚═╝   ╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝
- 
- -
-

API Status

-
-
- - {{query.isSuccess() ? 'Connected' : 'Disconnected'}} - -
-
- - -
-

Core Features

- -
- -
-

Type-Safe API

-

End-to-end type safety with tRPC

-
- - -
-

Modern React

-

TanStack Router + TanStack Query

-
- - -
-

Fast Backend

-

Lightweight Hono server

-
- - -
-

Beautiful UI

-

TailwindCSS + shadcn/ui components

-
-
-
- - - -
-
- `, - styles: [] -}) -export class HomeComponent { - private trpcService = inject(trpcService); - query = injectQuery(() => ({ - queryKey: ['healthCheck'], - queryFn: () => this.trpcService.proxy.healthCheck.query(), - })); - -} diff --git a/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs b/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs index f5d7c3712..48036f0c5 100644 --- a/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs +++ b/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs @@ -13,38 +13,7 @@ import { RpcService } from '../../services/rpc.service'; selector: 'app-home', standalone: true, imports: [CommonModule, RouterLink], - template: ` -
-
-
- ██████╗ ███████╗████████╗████████╗███████╗██████╗
- ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
- ██████╔╝█████╗     ██║      ██║   █████╗  ██████╔╝
- ██╔══██╗██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗
- ██████╔╝███████╗   ██║      ██║   ███████╗██║  ██║
- ╚═════╝ ╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝
-
- ████████╗    ███████╗████████╗ █████╗  ██████╗██╗  ██╗
- ╚══██╔══╝    ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
-    ██║       ███████╗   ██║   ███████║██║     █████╔╝
-    ██║       ╚════██║   ██║   ██╔══██║██║     ██╔═██╗
-    ██║       ███████║   ██║   ██║  ██║╚██████╗██║  ██╗
-    ╚═╝       ╚══════╝   ╚═╝   ╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝
- 
- {{#if (not (eq api "none"))}} -
-

API Status

-
-
- - {{query.isSuccess() ? 'Connected' : 'Disconnected'}} - -
-
- {{/if}} -
-
- `, + templateUrl: './home.component.html', styles: [] }) export class HomeComponent { diff --git a/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.html b/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.html new file mode 100644 index 000000000..0d8d09bdd --- /dev/null +++ b/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.html @@ -0,0 +1,15 @@ +
+
+

404

+

Page Not Found

+

+ The page you're looking for doesn't exist or has been moved. +

+ + Return Home + +
+
diff --git a/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts b/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts index e27dce928..e07fdb346 100644 --- a/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts +++ b/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts @@ -6,22 +6,6 @@ import { RouterLink } from '@angular/router'; selector: 'app-not-found', standalone: true, imports: [CommonModule, RouterLink], - template: ` -
-
-

404

-

Page Not Found

-

- The page you're looking for doesn't exist or has been moved. -

- - Return Home - -
-
- ` + templateUrl: './not-found-component.html' }) export class NotFoundComponent {} diff --git a/apps/cli/templates/frontend/angular/src/components/todo-list/todo-list.component.ts b/apps/cli/templates/frontend/angular/src/components/todo-list/todo-list.component.ts deleted file mode 100644 index 9a4fb8c66..000000000 --- a/apps/cli/templates/frontend/angular/src/components/todo-list/todo-list.component.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; -import { injectMutation, injectQuery, QueryClient } from '@tanstack/angular-query-experimental'; -import { todoSchema } from '../../models/validation.model'; -import { trpcService } from 'src/services/trpc.service'; -@Component({ - selector: 'app-todo-list', - standalone: true, - imports: [CommonModule, FormsModule, TanStackField], - template: ` -
-
-
-

Todo List

-

Manage your tasks efficiently

- -
-
- - -
- - @if (todo.api.state.meta.isDirty && todo.api.state.meta.errors) { - @for (error of todo.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
-
- -
- @if (queryToDo.data()?.length) { - @for (todo of queryToDo.data(); track $index) { -
- - - - {{ todo.text }} - - - -
- } - } @else { -
-

No tasks yet. Add your first task above!

-
- } -
-
-
- ` -}) -export class TodoListComponent { - private _trpc = inject(trpcService); - queryToDo = injectQuery(() => ({ - queryKey: ["todo"], - queryFn: () => this._trpc.proxy.todo.getAll.query(), - })); - constructor() { - console.log(this.queryToDo.data(), "todos"); - } - mutateToDo = injectMutation(() => ({ - mutationFn: (todo: string) => { - return this._trpc.proxy.todo.create.mutate({ text: todo }); - }, - onSuccess: () => { - this.queryClient.invalidateQueries({ queryKey: ["todo"] }); - this.todoForm.reset(); - }, - })); - updateToDo = injectMutation(() => ({ - mutationFn: (todo: Awaited>[number]) => { - console.log(todo, "todoForm"); - return this._trpc.proxy.todo.toggle.mutate({ - id: todo.id!, - completed: !todo.completed, - }); - }, - onSuccess: () => { - this.queryClient.invalidateQueries({ queryKey: ["todo"] }); - this.todoForm.reset(); - }, - })); - deleteTodo = injectMutation(() => ({ - mutationFn: (id: string) => { - return this._trpc.proxy.todo.delete.mutate({ id: id }); - }, - onSuccess: () => { - this.queryClient.invalidateQueries({ queryKey: ["todo"] }); - }, - })); - - queryClient = inject(QueryClient); - todoForm = injectForm({ - defaultValues: { - todo: "", - }, - validators: { - onChange: todoSchema, - }, - onSubmit: async ({ value }) => { - this.mutateToDo.mutate(value.todo); - }, - }); - canSubmit = injectStore(this.todoForm, (state) => state.canSubmit); - isSubmitting = injectStore(this.todoForm, (state) => state.isSubmitting); - -} diff --git a/apps/cli/templates/frontend/angular/src/guards/auth.guard.ts b/apps/cli/templates/frontend/angular/src/guards/auth.guard.ts deleted file mode 100644 index f4e668dae..000000000 --- a/apps/cli/templates/frontend/angular/src/guards/auth.guard.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { inject } from '@angular/core'; -import { Router, type CanActivateFn } from '@angular/router'; -import { AuthService } from '../services/auth.service'; - -export const authGuard: CanActivateFn = (route, state) => { - const authService = inject(AuthService); - const router = inject(Router); - - if (authService.isLoggedIn()) { - return true; - } - - router.navigate(['/login']); - return false; -}; \ No newline at end of file diff --git a/apps/cli/templates/frontend/angular/src/models/todo.model.ts b/apps/cli/templates/frontend/angular/src/models/todo.model.ts deleted file mode 100644 index 4d0e04880..000000000 --- a/apps/cli/templates/frontend/angular/src/models/todo.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Todo { - id: number; - text: string; - completed: boolean; -} diff --git a/apps/cli/templates/frontend/angular/src/models/validation.model.ts b/apps/cli/templates/frontend/angular/src/models/validation.model.ts deleted file mode 100644 index cd9119e16..000000000 --- a/apps/cli/templates/frontend/angular/src/models/validation.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { z } from "zod"; - -const signUpSchema = z - .object({ - name: z.string().min(2, "Name must be at least 2 characters"), - email: z.string().email("Invalid email address"), - password: z.string().min(8, "Password must be at least 8 characters") - }); -const loginSchema = z.object({ - email: z.string().email("Invalid email address"), - password: z.string().min(1, "Password is required"), - rememberMe: z.boolean(), -}); - -const todoSchema = z.object({ - todo: z.string().nonempty("Todo is required"), -}); -export { - signUpSchema, - loginSchema, - todoSchema, -}; - -type LoginSchema = z.infer; -type SignUpSchema = z.infer; -type TodoSchema = z.infer; -export type { LoginSchema, SignUpSchema, TodoSchema }; diff --git a/apps/cli/templates/frontend/angular/src/services/auth.service.ts b/apps/cli/templates/frontend/angular/src/services/auth.service.ts deleted file mode 100644 index e6594cbdb..000000000 --- a/apps/cli/templates/frontend/angular/src/services/auth.service.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { inject, Injectable, signal } from '@angular/core'; -import { Router } from '@angular/router'; -import { createAuthClient } from "better-auth/client"; -import type { User } from 'better-auth/types'; -import { toast } from 'ngx-sonner'; -import { BehaviorSubject } from 'rxjs'; - -@Injectable({ - providedIn: 'root' -}) -export class AuthService { - private isAuthenticated = new BehaviorSubject(false); - isAuthenticated$ = this.isAuthenticated.asObservable(); - user = signal(null); - authClient = createAuthClient({ - baseURL: 'http://localhost:3000', - }); - router = inject(Router); - constructor() { - this.getSession(); - } - - login(email: string, password: string): void { - this.authClient.signIn.email({ email, password }, { - onSuccess: (session) => { - this.isAuthenticated.next(true); - this.user.set(session.data?.user ?? null); - this.router.navigate(['/dashboard']); - }, - onError: (error) => { - console.error(error); - toast.error(error.error.message); - } - }); - } - signUp(email: string, password: string): void { - this.authClient.signUp.email({ email, password, name: email }, { - onSuccess: (session) => { - this.isAuthenticated.next(true); - this.user.set(session.data?.user ?? null); - this.router.navigate(['/dashboard']); - }, - onError: (error) => { - console.error(error); - toast.error(error.error.message); - } - }); - } - - getSession(): void { - this.authClient.getSession({}, { - onSuccess: (session) => { - this.isAuthenticated.next(true); - this.user.set(session.data?.user ?? null); - }, - onError: (error) => { - console.error(error); - toast.error(error.error.message); - } - }); - } - logout(): void { - this.authClient.signOut({}, { - onSuccess: () => { - this.isAuthenticated.next(false); - this.user.set(null); - this.router.navigate(['/login']); - }, - onError: (error) => { - console.error(error); - toast.error(error.error.message); - } - }); - } - isLoggedIn(): boolean { - return this.isAuthenticated.value; - } -} diff --git a/apps/cli/templates/frontend/angular/src/services/orpc.service.ts b/apps/cli/templates/frontend/angular/src/services/orpc.service.ts deleted file mode 100644 index c5215add7..000000000 --- a/apps/cli/templates/frontend/angular/src/services/orpc.service.ts +++ /dev/null @@ -1,45 +0,0 @@ -// import { Injectable } from '@angular/core'; -// import { createORPCClient } from "@orpc/client"; -// import { RPCLink } from "@orpc/client/fetch"; -// import { QueryCache, QueryClient } from '@tanstack/angular-query-experimental'; -// import { toast } from 'ngx-sonner'; -// import { appRouter, AppRouter } from '../../../server/src/routers'; -// import { environment } from '../environments/enviroments'; -// import type { RouterClient } from "@orpc/server"; - - -// @Injectable({ -// providedIn: 'root' -// }) -// export class ORPCService { -// private queryClient = new QueryClient({ -// queryCache: new QueryCache({ -// onError: (error) => { -// toast.error(`Error: ${error.message}`, { -// action: { -// label: "retry", -// onClick: () => { -// this.queryClient.invalidateQueries(); -// }, -// }, -// }); -// }, -// }), -// }); - -// private link = new RPCLink({ -// url: `${environment.baseUrl}/rpc`, -// fetch(url, options) { -// return fetch(url, { -// ...options, -// credentials: "include", -// }); -// }, -// }); - -// private client: RouterClient = createORPCClient(this.link); - -// getQueryClient(): QueryClient { -// return this.queryClient; -// } -// } diff --git a/apps/cli/templates/frontend/angular/src/services/todo.service.ts b/apps/cli/templates/frontend/angular/src/services/todo.service.ts deleted file mode 100644 index 1202230fd..000000000 --- a/apps/cli/templates/frontend/angular/src/services/todo.service.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; -import type { Todo } from '../models/todo.model'; - -@Injectable({ - providedIn: 'root' -}) -export class TodoService { - private todos = new BehaviorSubject([ - { id: 1, text: 'Add', completed: true }, - { id: 2, text: 'hgk', completed: false } - ]); - - todos$ = this.todos.asObservable(); - - constructor() { - // Load todos from localStorage if available - const savedTodos = localStorage.getItem('todos'); - if (savedTodos) { - this.todos.next(JSON.parse(savedTodos)); - } - } - - private saveTodos(todos: Todo[]): void { - localStorage.setItem('todos', JSON.stringify(todos)); - this.todos.next(todos); - } - - addTodo(text: string): void { - if (!text.trim()) return; - - const newTodo: Todo = { - id: Date.now(), - text: text.trim(), - completed: false - }; - - const updatedTodos = [...this.todos.value, newTodo]; - this.saveTodos(updatedTodos); - } - - toggleTodo(id: number): void { - const updatedTodos = this.todos.value.map(todo => - todo.id === id ? { ...todo, completed: !todo.completed } : todo - ); - this.saveTodos(updatedTodos); - } - - deleteTodo(id: number): void { - const updatedTodos = this.todos.value.filter(todo => todo.id !== id); - this.saveTodos(updatedTodos); - } -} diff --git a/apps/cli/templates/frontend/angular/src/services/trpc.service.ts b/apps/cli/templates/frontend/angular/src/services/trpc.service.ts deleted file mode 100644 index 2ed47d317..000000000 --- a/apps/cli/templates/frontend/angular/src/services/trpc.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable } from "@angular/core"; -import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import type { AppRouter } from "../../../server/src/routers/index"; - - - -const client = createTRPCClient({ - links: [ - httpBatchLink({ - url: "http://localhost:3000/", - fetch: (url, options) => { - return fetch(url, { - ...options, - credentials: "include", - }); - }, - }), - ], -}); - -@Injectable({ - providedIn: "root", -}) -export class trpcService { - public proxy = createTRPCClient({ - links: [ - httpBatchLink({ - url: "http://localhost:3000/trpc", - fetch: (url, options) => { - return fetch(url, { - ...options, - credentials: "include", - }); - }, - }), - ], - }); - -} From 22d6c3907a7ac962752e9e966a686d73c2792ab7 Mon Sep 17 00:00:00 2001 From: Vijayabaskar Date: Sun, 8 Jun 2025 00:33:43 +0530 Subject: [PATCH 4/9] angular template missing pieces added --- apps/cli/src/constants.ts | 1 + .../project-generation/template-manager.ts | 2 +- apps/cli/src/helpers/setup/api-setup.ts | 24 ++++++++++++++++--- apps/cli/src/utils/template-processor.ts | 2 ++ .../todo-list/todo-list.component.html | 0 .../todo-list/todo-list.component.ts.hbs | 0 .../templates/frontend/angular/package.json | 1 - .../frontend/angular/src/app.config.ts.hbs | 4 ++-- .../frontend/angular/src/index.html.hbs | 4 ++-- 9 files changed, 29 insertions(+), 9 deletions(-) rename apps/cli/templates/examples/todo/web/angular/{ => src}/components/todo-list/todo-list.component.html (100%) rename apps/cli/templates/examples/todo/web/angular/{ => src}/components/todo-list/todo-list.component.ts.hbs (100%) diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index dfbf20bae..7a29a3b27 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -93,6 +93,7 @@ export const dependencyVersionMap = { "@orpc/tanstack-query": "^1.4.1", "@trpc/tanstack-react-query": "^11.0.0", + "@tanstack/angular-query-experimental": "^5.80.2", "@trpc/server": "^11.0.0", "@trpc/client": "^11.0.0", diff --git a/apps/cli/src/helpers/project-generation/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts index d1c44f5df..65a6405ad 100644 --- a/apps/cli/src/helpers/project-generation/template-manager.ts +++ b/apps/cli/src/helpers/project-generation/template-manager.ts @@ -189,7 +189,7 @@ export async function setupFrontendTemplates( } else { } - if (!isConvex && context.api === "none") { + if (!isConvex && (context.api === "orpc" || context.api === "trpc")) { const apiWebAngularDir = path.join( PKG_ROOT, `templates/api/${context.api}/web/angular`, diff --git a/apps/cli/src/helpers/setup/api-setup.ts b/apps/cli/src/helpers/setup/api-setup.ts index 9cd620a34..d8c9f3ce9 100644 --- a/apps/cli/src/helpers/setup/api-setup.ts +++ b/apps/cli/src/helpers/setup/api-setup.ts @@ -107,7 +107,15 @@ export async function setupApi(config: ProjectConfig): Promise { }); } } else if (hasAngularWeb) { - if (api === "orpc") { + if(api === "trpc"){ + await addPackageDependency({ + dependencies: [ + "@trpc/client", + "@trpc/server", + ], + projectDir: webDir, + }); + } else if (api === "orpc") { await addPackageDependency({ dependencies: [ "@orpc/tanstack-query", @@ -153,7 +161,7 @@ export async function setupApi(config: ProjectConfig): Promise { ]; const needsSolidQuery = frontend.includes("solid"); const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f)); - + const needsAngularQuery = frontend.includes("angular"); if (needsReactQuery && !isConvex) { const reactQueryDeps: AvailableDependencies[] = ["@tanstack/react-query"]; const reactQueryDevDeps: AvailableDependencies[] = [ @@ -217,7 +225,17 @@ export async function setupApi(config: ProjectConfig): Promise { } } } - + if(needsAngularQuery && !isConvex){ + if(webDirExists){ + const webPkgJsonPath = path.join(webDir, "package.json"); + if (await fs.pathExists(webPkgJsonPath)) { + await addPackageDependency({ + dependencies: ["@tanstack/angular-query-experimental"], + projectDir: webDir, + }); + } + } + } if (isConvex) { if (webDirExists) { const webPkgJsonPath = path.join(webDir, "package.json"); diff --git a/apps/cli/src/utils/template-processor.ts b/apps/cli/src/utils/template-processor.ts index aaf700950..9acc655b3 100644 --- a/apps/cli/src/utils/template-processor.ts +++ b/apps/cli/src/utils/template-processor.ts @@ -32,6 +32,8 @@ handlebars.registerHelper("or", (a, b) => a || b); handlebars.registerHelper("eq", (a, b) => a === b); +handlebars.registerHelper("not", (a) => !a); + handlebars.registerHelper( "includes", (array, value) => Array.isArray(array) && array.includes(value), diff --git a/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.html b/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html similarity index 100% rename from apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.html rename to apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html diff --git a/apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs b/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.ts.hbs similarity index 100% rename from apps/cli/templates/examples/todo/web/angular/components/todo-list/todo-list.component.ts.hbs rename to apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.ts.hbs diff --git a/apps/cli/templates/frontend/angular/package.json b/apps/cli/templates/frontend/angular/package.json index c18aeca4d..8ff8cee2c 100644 --- a/apps/cli/templates/frontend/angular/package.json +++ b/apps/cli/templates/frontend/angular/package.json @@ -21,7 +21,6 @@ "@angular/router": "^20.0.0", "@tailwindcss/postcss": "^4.1.8", "@tanstack/angular-form": "^1.12.1", - "@tanstack/angular-query-experimental": "^5.80.2", "ngx-sonner": "^3.1.0", "postcss": "^8.5.4", "rxjs": "~7.8.2", diff --git a/apps/cli/templates/frontend/angular/src/app.config.ts.hbs b/apps/cli/templates/frontend/angular/src/app.config.ts.hbs index 7b80797b4..79e60d71d 100644 --- a/apps/cli/templates/frontend/angular/src/app.config.ts.hbs +++ b/apps/cli/templates/frontend/angular/src/app.config.ts.hbs @@ -15,7 +15,7 @@ import { withDevtools, } from "@tanstack/angular-query-experimental"; {{/if}} -{{#if (eq addons "pwa")}} +{{#if (eq addons.pwa "true")}} import { provideServiceWorker } from '@angular/service-worker'; import { isDevMode } from '@angular/core'; {{/if}} @@ -42,7 +42,7 @@ export const appConfig: ApplicationConfig = { withDevtools(() => ({ loadDevtools: "auto" })), ), {{/if}} - {{#if (eq addons "pwa")}} + {{#if (eq addons.pwa "true")}} provideServiceWorker('ngsw-worker.js', { enabled: !isDevMode(), registrationStrategy: 'registerWhenStable:30000', diff --git a/apps/cli/templates/frontend/angular/src/index.html.hbs b/apps/cli/templates/frontend/angular/src/index.html.hbs index 329727baa..9e7e70eeb 100644 --- a/apps/cli/templates/frontend/angular/src/index.html.hbs +++ b/apps/cli/templates/frontend/angular/src/index.html.hbs @@ -6,13 +6,13 @@ - {{#if addons.includes("pwa")}} + {{#if (eq addons.pwa "true")}} {{/if}} - {{#if addons.includes("pwa")}} + {{#if (eq addons.pwa "true")}} {{/if}} From 2ca1cd7f0fb6c8e9ef3209d39843d2ec41b61e02 Mon Sep 17 00:00:00 2001 From: Vijayabaskar Date: Sun, 8 Jun 2025 21:53:30 +0530 Subject: [PATCH 5/9] trpc setup remove --- apps/cli/src/validation.ts | 6 +- .../angular/src/services/rpc.service.ts.hbs | 3 + .../angular/src/services/rpc.service.ts.hbs | 38 ------- .../components/auth/login/login.component.ts | 2 +- ...up.coponent.html => signup.component.html} | 0 .../auth/web/angular/src/guards/auth.guard.ts | 2 +- .../todo-list/todo-list.component.html | 10 +- .../todo-list/todo-list.component.ts.hbs | 51 +-------- .../templates/frontend/angular/angular.json | 106 ------------------ .../angular/src/app/app.component.css | 0 .../angular/src/app/app.component.html | 7 -- .../angular/src/app/app.component.spec.ts | 29 ----- .../frontend/angular/src/app/app.component.ts | 24 ---- .../frontend/angular/src/app/app.config.ts | 44 -------- .../frontend/angular/src/app/app.routes.ts | 22 ---- ...mponent.html => header.component.html.hbs} | 8 +- ...component.html => home.component.html.hbs} | 4 +- .../src/components/home/home.component.ts.hbs | 12 +- .../templates/frontend/angular/src/main.ts | 4 +- .../frontend/angular/tsconfig.app.json | 3 - .../templates/frontend/angular/tsconfig.json | 3 - 21 files changed, 25 insertions(+), 353 deletions(-) delete mode 100644 apps/cli/templates/api/trpc/web/angular/src/services/rpc.service.ts.hbs rename apps/cli/templates/auth/web/angular/src/components/auth/signup/{signup.coponent.html => signup.component.html} (100%) delete mode 100644 apps/cli/templates/frontend/angular/angular.json delete mode 100644 apps/cli/templates/frontend/angular/src/app/app.component.css delete mode 100644 apps/cli/templates/frontend/angular/src/app/app.component.html delete mode 100644 apps/cli/templates/frontend/angular/src/app/app.component.spec.ts delete mode 100644 apps/cli/templates/frontend/angular/src/app/app.component.ts delete mode 100644 apps/cli/templates/frontend/angular/src/app/app.config.ts delete mode 100644 apps/cli/templates/frontend/angular/src/app/app.routes.ts rename apps/cli/templates/frontend/angular/src/components/header/{header.component.html => header.component.html.hbs} (94%) rename apps/cli/templates/frontend/angular/src/components/home/{home.component.html => home.component.html.hbs} (95%) diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index 9165d5379..8d87a6c78 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -401,14 +401,14 @@ export function validateConfigCompatibility( const includesSolid = effectiveFrontend?.includes("solid"); const includesAngular = effectiveFrontend?.includes("angular"); if ( - (includesNuxt || includesSvelte || includesSolid) && + (includesNuxt || includesSvelte || includesSolid || includesAngular) && effectiveApi === "trpc" ) { consola.fatal( `tRPC API is not supported with '${ - includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid" + includesNuxt ? "nuxt" : includesSvelte ? "svelte" : includesSolid ? "solid" : "angular" }' frontend. Please use --api orpc or --api none or remove '${ - includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid" + includesNuxt ? "nuxt" : includesSvelte ? "svelte" : includesSolid ? "solid" : "angular" }' from --frontend.`, ); process.exit(1); diff --git a/apps/cli/templates/api/orpc/web/angular/src/services/rpc.service.ts.hbs b/apps/cli/templates/api/orpc/web/angular/src/services/rpc.service.ts.hbs index 45c03d93a..87ac98189 100644 --- a/apps/cli/templates/api/orpc/web/angular/src/services/rpc.service.ts.hbs +++ b/apps/cli/templates/api/orpc/web/angular/src/services/rpc.service.ts.hbs @@ -6,6 +6,7 @@ import { toast } from 'ngx-sonner'; import { appRouter, AppRouter } from '../../../server/src/routers'; import { environment } from '../environments/enviroments'; import type { RouterClient } from "@orpc/server"; +import { createTanstackQueryUtils } from '@orpc/tanstack-query'; export const queryClient = new QueryClient({ queryCache: new QueryCache({ @@ -37,4 +38,6 @@ export class RpcService { }); private client: RouterClient = createORPCClient(this.link); + public utils = createTanstackQueryUtils(this.client); + } diff --git a/apps/cli/templates/api/trpc/web/angular/src/services/rpc.service.ts.hbs b/apps/cli/templates/api/trpc/web/angular/src/services/rpc.service.ts.hbs deleted file mode 100644 index 32f487083..000000000 --- a/apps/cli/templates/api/trpc/web/angular/src/services/rpc.service.ts.hbs +++ /dev/null @@ -1,38 +0,0 @@ -import { Injectable } from "@angular/core"; -import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import type { AppRouter } from "../../../server/src/routers/index"; -import { QueryClient, QueryCache } from "@tanstack/angular-query-experimental"; -import { toast } from "ngx-sonner"; -export const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError: (error) => { - toast.error(error.message, { - action: { - label: "retry", - onClick: () => { - queryClient.invalidateQueries(); - }, - }, - }); - }, - }), -}); - -@Injectable({ - providedIn: "root", -}) -export class RpcService { - public proxy = createTRPCClient({ - links: [ - httpBatchLink({ - url: "http://localhost:3000/trpc", - fetch: (url, options) => { - return fetch(url, { - ...options, - credentials: "include", - }); - }, - }), - ], - }); -} diff --git a/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts b/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts index 8d862e1ac..29293eacf 100644 --- a/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts +++ b/apps/cli/templates/auth/web/angular/src/components/auth/login/login.component.ts @@ -2,7 +2,7 @@ import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { RouterLink } from '@angular/router'; -import { AuthService } from '../../services/auth.service'; +import { AuthService } from '../../../services/auth.service'; import { injectForm, injectStore, TanStackField } from '@tanstack/angular-form'; import { z } from 'zod'; diff --git a/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.coponent.html b/apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.html similarity index 100% rename from apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.coponent.html rename to apps/cli/templates/auth/web/angular/src/components/auth/signup/signup.component.html diff --git a/apps/cli/templates/auth/web/angular/src/guards/auth.guard.ts b/apps/cli/templates/auth/web/angular/src/guards/auth.guard.ts index 63082b00b..aec1290ca 100644 --- a/apps/cli/templates/auth/web/angular/src/guards/auth.guard.ts +++ b/apps/cli/templates/auth/web/angular/src/guards/auth.guard.ts @@ -1,6 +1,6 @@ import { inject } from '@angular/core'; import { Router, type CanActivateFn } from '@angular/router'; -import { AuthService } from '../../../../../frontend/angular/src/services/auth.service'; +import { AuthService } from '../services/auth.service'; export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); diff --git a/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html b/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html index fa19442ab..ecf0202b6 100644 --- a/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html +++ b/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html @@ -44,7 +44,7 @@

Todo List< [class.border-gray-300]="!todo.completed" [class.dark:border-gray-600]="!todo.completed" [class.border-primary-500]="todo.completed" - (click)="updateToDo.mutate(todo)" + (click)="updateToDo.mutate({ id: todo.id.toString(), completed: !todo.completed })" aria-label="Toggle todo completion" > Todo List<
- -
- -
- -
diff --git a/apps/cli/templates/frontend/angular/src/app/app.component.spec.ts b/apps/cli/templates/frontend/angular/src/app/app.component.spec.ts deleted file mode 100644 index 5119ea23a..000000000 --- a/apps/cli/templates/frontend/angular/src/app/app.component.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AppComponent], - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); - - it(`should have the 'my-app' title`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('my-app'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, my-app'); - }); -}); diff --git a/apps/cli/templates/frontend/angular/src/app/app.component.ts b/apps/cli/templates/frontend/angular/src/app/app.component.ts deleted file mode 100644 index 2a09fd76d..000000000 --- a/apps/cli/templates/frontend/angular/src/app/app.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component, inject } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; -import { NgxSonnerToaster } from 'ngx-sonner'; -import { HeaderComponent } from '../components/header/header.component'; -import { ThemeService } from '../services/theme.service'; - -@Component({ - selector: 'app-root', - standalone: true, - imports: [ - CommonModule, - RouterOutlet, - HeaderComponent, - NgxSonnerToaster - ], - templateUrl: './app.component.html', -}) -export class AppComponent { - private themeService = inject(ThemeService); - constructor() { - this.themeService.initTheme(); - } -} diff --git a/apps/cli/templates/frontend/angular/src/app/app.config.ts b/apps/cli/templates/frontend/angular/src/app/app.config.ts deleted file mode 100644 index 13479579f..000000000 --- a/apps/cli/templates/frontend/angular/src/app/app.config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - type ApplicationConfig, - provideZoneChangeDetection, -} from "@angular/core"; -import { provideAnimations } from "@angular/platform-browser/animations"; -import { provideRouter } from "@angular/router"; - -// import { provideTrpcClient } from "./utils/trpc-client"; -import { provideHttpClient, withInterceptors } from "@angular/common/http"; -import { - QueryClient, - provideTanStackQuery, - withDevtools, -} from "@tanstack/angular-query-experimental"; -import { routes } from "./app.routes"; - -import type { - HttpHandlerFn, - HttpInterceptorFn, - HttpRequest, -} from "@angular/common/http"; - -const withCredentialsInterceptor: HttpInterceptorFn = ( - req: HttpRequest, - next: HttpHandlerFn, -) => { - const modifiedReq = req.clone({ - withCredentials: true, - }); - return next(modifiedReq); -}; -export const appConfig: ApplicationConfig = { - providers: [ - provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(routes), - provideAnimations(), - provideHttpClient(withInterceptors([withCredentialsInterceptor])), - // provideTrpcClient(), - provideTanStackQuery( - new QueryClient(), - withDevtools(() => ({ loadDevtools: "auto" })), - ), - ], -}; diff --git a/apps/cli/templates/frontend/angular/src/app/app.routes.ts b/apps/cli/templates/frontend/angular/src/app/app.routes.ts deleted file mode 100644 index e6ea12cb1..000000000 --- a/apps/cli/templates/frontend/angular/src/app/app.routes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Route } from '@angular/router'; -import { HomeComponent } from '../components/home/home.component'; -import { AIChatComponent } from '../components/ai-chat/ai-chat.component'; -import { LoginComponent } from '../components/auth/login/login.component'; -import { TodoListComponent } from '../components/todo-list/todo-list.component'; -import { DashboardComponent } from '../components/dashboard/dashboard.component'; -import { SignupComponent } from '../components/auth/signup/signup.component'; -import { authGuard } from '../guards/auth.guard'; - - -export const routes: Route[] = [ - { path: '', component: HomeComponent }, - { path: 'login', component: LoginComponent }, - { path: 'signup', component: SignupComponent }, - { path: 'todos', component: TodoListComponent }, - { - path: 'dashboard', - component: DashboardComponent, - canActivate: [authGuard] - }, - { path: 'ai-chat', component: AIChatComponent } -]; diff --git a/apps/cli/templates/frontend/angular/src/components/header/header.component.html b/apps/cli/templates/frontend/angular/src/components/header/header.component.html.hbs similarity index 94% rename from apps/cli/templates/frontend/angular/src/components/header/header.component.html rename to apps/cli/templates/frontend/angular/src/components/header/header.component.html.hbs index cd9013544..4feab5be0 100644 --- a/apps/cli/templates/frontend/angular/src/components/header/header.component.html +++ b/apps/cli/templates/frontend/angular/src/components/header/header.component.html.hbs @@ -84,9 +84,9 @@ class="flex items-center space-x-2 p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" >
- {{ userInitial }} + \{{ userInitial }}
- {{ authService.user()?.name }} + \{{ authService.user()?.name }}
-
{{ this.authService.user()?.name }}
-
{{ this.authService.user()?.email }}
+
\{{ this.authService.user()?.name }}
+
\{{ this.authService.user()?.email }}
- {{#if (not (eq auth "none"))}} + {{#if auth}} {{#if (eq api "orpc")}}
diff --git a/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs b/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs index 0cc67766f..c56684c73 100644 --- a/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs +++ b/apps/cli/templates/frontend/angular/src/components/home/home.component.ts.hbs @@ -9,7 +9,7 @@ import { RpcService } from '../../services/rpc.service'; @Component({ selector: 'app-home', standalone: true, - imports: [CommonModule, RouterLink], + imports: [CommonModule], templateUrl: './home.component.html', styles: [] }) From 458e7cd83503695aed1cccca7bc063ddf81c52de Mon Sep 17 00:00:00 2001 From: Vijayabaskar Date: Sun, 8 Jun 2025 23:05:58 +0530 Subject: [PATCH 7/9] angular added to stack builder --- apps/web/public/icon/angular.svg | 15 +++++++ .../app/(home)/_components/stack-builder.tsx | 44 ++++++++++++++++--- apps/web/src/lib/constant.ts | 8 ++++ 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 apps/web/public/icon/angular.svg diff --git a/apps/web/public/icon/angular.svg b/apps/web/public/icon/angular.svg new file mode 100644 index 000000000..4b62f5a30 --- /dev/null +++ b/apps/web/public/icon/angular.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index 66c4b1e49..6eb4fcc73 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -91,6 +91,7 @@ const hasWebFrontend = (webFrontend: string[]) => "nuxt", "svelte", "solid", + "angular", ].includes(f), ); @@ -100,7 +101,7 @@ const checkHasNativeFrontend = (nativeFrontend: string[]) => const hasPWACompatibleFrontend = (webFrontend: string[]) => webFrontend.some((f) => - ["tanstack-router", "react-router", "solid", "next"].includes(f), + ["tanstack-router", "react-router", "solid", "next", "angular"].includes(f), ); const hasTauriCompatibleFrontend = (webFrontend: string[]) => @@ -112,6 +113,7 @@ const hasTauriCompatibleFrontend = (webFrontend: string[]) => "svelte", "solid", "next", + "angular", ].includes(f), ); @@ -555,8 +557,9 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { const isNuxt = nextStack.webFrontend.includes("nuxt"); const isSvelte = nextStack.webFrontend.includes("svelte"); const isSolid = nextStack.webFrontend.includes("solid"); - if ((isNuxt || isSvelte || isSolid) && nextStack.api === "trpc") { - const frontendName = isNuxt ? "Nuxt" : isSvelte ? "Svelte" : "Solid"; + const isAngular = nextStack.webFrontend.includes("angular"); + if ((isNuxt || isSvelte || isSolid || isAngular) && nextStack.api === "trpc") { + const frontendName = isNuxt ? "Nuxt" : isSvelte ? "Svelte" : isSolid ? "Solid" : "Angular"; notes.api.notes.push( `${frontendName} requires oRPC. It will be selected automatically.`, ); @@ -652,6 +655,13 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { message: "AI example removed (not compatible with Solid)", }); } + if (isAngular && nextStack.examples.includes("ai")) { + incompatibleExamples.push("ai"); + changes.push({ + category: "examples", + message: "AI example removed (not compatible with Angular)", + }); + } const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)]; if (uniqueIncompatibleExamples.length > 0) { @@ -691,6 +701,16 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { notes.webFrontend.hasIssue = true; notes.examples.hasIssue = true; } + if (isAngular && uniqueIncompatibleExamples.includes("ai")) { + notes.webFrontend.notes.push( + "AI example is not compatible with Angular. It will be removed.", + ); + notes.examples.notes.push( + "AI example is not compatible with Angular. It will be removed.", + ); + notes.webFrontend.hasIssue = true; + notes.examples.hasIssue = true; + } const originalExamplesLength = nextStack.examples.length; nextStack.examples = nextStack.examples.filter( @@ -717,7 +737,7 @@ const getCompatibilityRules = (stack: StackState) => { const hasSolid = stack.webFrontend.includes("solid"); const hasNuxt = stack.webFrontend.includes("nuxt"); const hasSvelte = stack.webFrontend.includes("svelte"); - + const hasAngular = stack.webFrontend.includes("angular"); return { isConvex, isBackendNone, @@ -725,10 +745,11 @@ const getCompatibilityRules = (stack: StackState) => { hasNativeFrontend, hasPWACompatible: hasPWACompatibleFrontend(stack.webFrontend), hasTauriCompatible: hasTauriCompatibleFrontend(stack.webFrontend), - hasNuxtOrSvelteOrSolid: hasNuxt || hasSvelte || hasSolid, + hasNuxtOrSvelteOrSolidOrAngular: hasNuxt || hasSvelte || hasSolid || hasAngular, hasSolid, hasNuxt, hasSvelte, + hasAngular, }; }; @@ -1091,12 +1112,14 @@ const StackBuilder = () => { : "Disabled: No backend requires API to be 'None'.", ); } - if (techId === "trpc" && rules.hasNuxtOrSvelteOrSolid) { + if (techId === "trpc" && rules.hasNuxtOrSvelteOrSolidOrAngular) { const frontendName = rules.hasNuxt ? "Nuxt" : rules.hasSvelte ? "Svelte" - : "Solid"; + : rules.hasSolid + ? "Solid" + : "Angular"; addRule( category, techId, @@ -1303,6 +1326,13 @@ const StackBuilder = () => { "Disabled: The 'AI' example is not compatible with a Solid frontend.", ); } + if (rules.hasAngular && techId === "ai") { + addRule( + category, + techId, + "Disabled: The 'AI' example is not compatible with an Angular frontend.", + ); + } } } } diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 10c80bde2..2b1a03be7 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -81,6 +81,14 @@ export const TECH_OPTIONS = { color: "from-blue-600 to-blue-800", default: false, }, + { + id: "angular", + name: "Angular", + description: "The web framework that empowers developers to build fast, reliable applications", + icon: "/icon/angular.svg", + color: "from-red-500 to-red-700", + default: false, + }, { id: "none", name: "No Web Frontend", From 8051804643bb53c563741fb578dcc161019bf343 Mon Sep 17 00:00:00 2001 From: Vijayabaskar Date: Sun, 8 Jun 2025 23:59:51 +0530 Subject: [PATCH 8/9] code rabbit feedback fixes --- apps/cli/src/constants.ts | 4 +- ...t.webmanifest => manifest.webmanifest.hbs} | 4 +- .../angular/src/services/rpc.service.ts.hbs | 2 +- .../web/angular/src/services/auth.service.ts | 11 +- .../todo-list/todo-list.component.html | 142 ++++++++---------- .../frontend/angular/angular.json.hbs | 4 +- .../frontend/angular/src/app.routes.ts.hbs | 6 +- ...omponent.html => not-found.component.html} | 0 ...nd-component.ts => not-found.component.ts} | 2 +- .../{enviroments.ts => environment.ts} | 0 .../templates/frontend/angular/src/main.ts | 2 +- 11 files changed, 77 insertions(+), 100 deletions(-) rename apps/cli/templates/addons/pwa/apps/web/angular/public/{manifest.webmanifest => manifest.webmanifest.hbs} (94%) rename apps/cli/templates/frontend/angular/src/components/not-found/{not-found-component.html => not-found.component.html} (100%) rename apps/cli/templates/frontend/angular/src/components/not-found/{not-found-component.ts => not-found.component.ts} (86%) rename apps/cli/templates/frontend/angular/src/environments/{enviroments.ts => environment.ts} (100%) diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 7a29a3b27..abbcd46ad 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -45,7 +45,7 @@ export const dependencyVersionMap = { mongoose: "^8.14.0", "vite-plugin-pwa": "^0.21.2", - "@angular/service-worker" : "^19.2.0", + "@angular/service-worker" : "^20.0.2", "@vite-pwa/assets-generator": "^0.2.6", "@tauri-apps/cli": "^2.4.0", @@ -93,7 +93,7 @@ export const dependencyVersionMap = { "@orpc/tanstack-query": "^1.4.1", "@trpc/tanstack-react-query": "^11.0.0", - "@tanstack/angular-query-experimental": "^5.80.2", + "@tanstack/angular-query-experimental": "5.80.2", "@trpc/server": "^11.0.0", "@trpc/client": "^11.0.0", diff --git a/apps/cli/templates/addons/pwa/apps/web/angular/public/manifest.webmanifest b/apps/cli/templates/addons/pwa/apps/web/angular/public/manifest.webmanifest.hbs similarity index 94% rename from apps/cli/templates/addons/pwa/apps/web/angular/public/manifest.webmanifest rename to apps/cli/templates/addons/pwa/apps/web/angular/public/manifest.webmanifest.hbs index 4fe65095d..a3a66e138 100644 --- a/apps/cli/templates/addons/pwa/apps/web/angular/public/manifest.webmanifest +++ b/apps/cli/templates/addons/pwa/apps/web/angular/public/manifest.webmanifest.hbs @@ -1,6 +1,6 @@ { - "name": "pwa-angu", - "short_name": "pwa-angu", + "name": "{{projectName}}", + "short_name": "{{projectName}}", "display": "standalone", "scope": "./", "start_url": "./", diff --git a/apps/cli/templates/api/orpc/web/angular/src/services/rpc.service.ts.hbs b/apps/cli/templates/api/orpc/web/angular/src/services/rpc.service.ts.hbs index 87ac98189..e2b4bdaf6 100644 --- a/apps/cli/templates/api/orpc/web/angular/src/services/rpc.service.ts.hbs +++ b/apps/cli/templates/api/orpc/web/angular/src/services/rpc.service.ts.hbs @@ -4,7 +4,7 @@ import { RPCLink } from "@orpc/client/fetch"; import { QueryCache, QueryClient } from '@tanstack/angular-query-experimental'; import { toast } from 'ngx-sonner'; import { appRouter, AppRouter } from '../../../server/src/routers'; -import { environment } from '../environments/enviroments'; +import { environment } from '../environments/environment'; import type { RouterClient } from "@orpc/server"; import { createTanstackQueryUtils } from '@orpc/tanstack-query'; diff --git a/apps/cli/templates/auth/web/angular/src/services/auth.service.ts b/apps/cli/templates/auth/web/angular/src/services/auth.service.ts index bc966f70f..256f2f28f 100644 --- a/apps/cli/templates/auth/web/angular/src/services/auth.service.ts +++ b/apps/cli/templates/auth/web/angular/src/services/auth.service.ts @@ -1,25 +1,24 @@ -import { inject, Injectable, signal } from '@angular/core'; +import { inject, Injectable, OnInit, signal } from '@angular/core'; import { Router } from '@angular/router'; import { createAuthClient } from "better-auth/client"; import type { User } from 'better-auth/types'; import { toast } from 'ngx-sonner'; import { BehaviorSubject } from 'rxjs'; - +import { environment } from '../environments/environment'; @Injectable({ providedIn: 'root' }) -export class AuthService { +export class AuthService implements OnInit { private isAuthenticated = new BehaviorSubject(false); isAuthenticated$ = this.isAuthenticated.asObservable(); user = signal(null); authClient = createAuthClient({ - baseURL: 'http://localhost:3000', + baseURL: environment.baseUrl, }); router = inject(Router); - constructor() { + ngOnInit(): void { this.getSession(); } - login(email: string, password: string): void { this.authClient.signIn.email({ email, password }, { onSuccess: (session) => { diff --git a/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html b/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html index ecf0202b6..271664cc2 100644 --- a/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html +++ b/apps/cli/templates/examples/todo/web/angular/src/components/todo-list/todo-list.component.html @@ -1,90 +1,68 @@
-
-
-

Todo List

-

Manage your tasks efficiently

+
+
+

Todo List

+

Manage your tasks efficiently

-
-
- - -
+
+
+ + +
- @if (todo.api.state.meta.isDirty && todo.api.state.meta.errors) { - @for (error of todo.api.state.meta.errors; track $index) { - {{ error.message }} - } - } -
+ @if (todo.api.state.meta.isDirty && todo.api.state.meta.errors) { + @for (error of todo.api.state.meta.errors; track $index) { + {{ error.message }} + } + }
+
-
- @if (queryToDo.data()?.length) { - @for (todo of queryToDo.data(); track $index) { -
- - - - {{ todo.text }} - - - -
- } - } @else { -
-

No tasks yet. Add your first task above!

-
- } +
+ @if (queryToDo.data()?.length) { + @for (todo of queryToDo.data(); track $index) { + @if(todo && todo.id) { +
+ + + {{ todo.text }} + + +
+ } + } + } @else { +
+

No tasks yet. Add your first task above!

+ } +
diff --git a/apps/cli/templates/frontend/angular/angular.json.hbs b/apps/cli/templates/frontend/angular/angular.json.hbs index 00d6a192d..93aa81a00 100644 --- a/apps/cli/templates/frontend/angular/angular.json.hbs +++ b/apps/cli/templates/frontend/angular/angular.json.hbs @@ -29,8 +29,8 @@ ], "scripts": [], "allowedCommonJsDependencies": [ - "@trpc/server", - "@trpc/client" + "@orpc/server", + "@orpc/client" ], "preserveSymlinks": true }, diff --git a/apps/cli/templates/frontend/angular/src/app.routes.ts.hbs b/apps/cli/templates/frontend/angular/src/app.routes.ts.hbs index 1fb4f37a5..9f48e9ff2 100644 --- a/apps/cli/templates/frontend/angular/src/app.routes.ts.hbs +++ b/apps/cli/templates/frontend/angular/src/app.routes.ts.hbs @@ -1,7 +1,7 @@ import type { Route } from '@angular/router'; import { HomeComponent } from './components/home/home.component'; -import { NotFoundComponent } from './components/not-found/not-found-component'; -{{#if (eq examples "todos")}} +import { NotFoundComponent } from './components/not-found/not-found.component'; +{{#if (not (eq examples "none"))}} import { TodoListComponent } from './components/todo-list/todo-list.component'; {{/if}} {{#if auth}} @@ -13,7 +13,7 @@ import { authGuard } from './guards/auth.guard'; export const routes: Route[] = [ { path: '', component: HomeComponent }, - {{#if (eq examples "todos")}} + {{#if (not (eq examples "none"))}} { path: 'todos', component: TodoListComponent }, {{/if}} {{#if auth}} diff --git a/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.html b/apps/cli/templates/frontend/angular/src/components/not-found/not-found.component.html similarity index 100% rename from apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.html rename to apps/cli/templates/frontend/angular/src/components/not-found/not-found.component.html diff --git a/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts b/apps/cli/templates/frontend/angular/src/components/not-found/not-found.component.ts similarity index 86% rename from apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts rename to apps/cli/templates/frontend/angular/src/components/not-found/not-found.component.ts index e07fdb346..d7687eb5f 100644 --- a/apps/cli/templates/frontend/angular/src/components/not-found/not-found-component.ts +++ b/apps/cli/templates/frontend/angular/src/components/not-found/not-found.component.ts @@ -6,6 +6,6 @@ import { RouterLink } from '@angular/router'; selector: 'app-not-found', standalone: true, imports: [CommonModule, RouterLink], - templateUrl: './not-found-component.html' + templateUrl: './not-found.component.html' }) export class NotFoundComponent {} diff --git a/apps/cli/templates/frontend/angular/src/environments/enviroments.ts b/apps/cli/templates/frontend/angular/src/environments/environment.ts similarity index 100% rename from apps/cli/templates/frontend/angular/src/environments/enviroments.ts rename to apps/cli/templates/frontend/angular/src/environments/environment.ts diff --git a/apps/cli/templates/frontend/angular/src/main.ts b/apps/cli/templates/frontend/angular/src/main.ts index 63bb8411e..1f6a1b050 100644 --- a/apps/cli/templates/frontend/angular/src/main.ts +++ b/apps/cli/templates/frontend/angular/src/main.ts @@ -2,7 +2,7 @@ import { enableProdMode } from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { appConfig } from './app.config'; -import { environment } from './environments/enviroments'; +import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); From f0abd36e8178b35f959bc5db0467284db6e1eb77 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 13 Jun 2025 17:18:51 +0530 Subject: [PATCH 9/9] fix --- apps/cli/src/cli.ts | 135 ------------------ .../project-generation/template-manager.ts | 42 +++++- apps/cli/src/helpers/setup/api-setup.ts | 29 ++-- apps/cli/src/prompts/examples.ts | 6 +- apps/cli/src/types.ts | 1 + .../app/(home)/_components/stack-builder.tsx | 16 ++- 6 files changed, 67 insertions(+), 162 deletions(-) delete mode 100644 apps/cli/src/cli.ts diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts deleted file mode 100644 index 8354c4c15..000000000 --- a/apps/cli/src/cli.ts +++ /dev/null @@ -1,135 +0,0 @@ -import yargs from "yargs"; -import { hideBin } from "yargs/helpers"; -import type { YargsArgv } from "./types"; -import { getLatestCLIVersion } from "./utils/get-latest-cli-version"; - -export async function parseCliArguments(): Promise { - const argv = await yargs(hideBin(process.argv)) - .scriptName("create-better-t-stack") - .usage( - "$0 [project-directory] [options]", - "Create a new Better-T Stack project", - ) - .positional("project-directory", { - describe: "Project name/directory", - type: "string", - }) - .option("yes", { - alias: "y", - type: "boolean", - describe: "Use default configuration and skip prompts", - default: false, - }) - .option("database", { - type: "string", - describe: "Database type", - choices: ["none", "sqlite", "postgres", "mysql", "mongodb"], - }) - .option("orm", { - type: "string", - describe: "ORM type", - choices: ["drizzle", "prisma", "mongoose", "none"], - }) - .option("auth", { - type: "boolean", - describe: "Include authentication (use --no-auth to exclude)", - }) - .option("frontend", { - type: "array", - string: true, - describe: "Frontend types", - choices: [ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "native-nativewind", - "native-unistyles", - "svelte", - "angular", - "solid", - "none", - ], - }) - .option("addons", { - type: "array", - string: true, - describe: "Additional addons", - choices: [ - "pwa", - "tauri", - "starlight", - "biome", - "husky", - "turborepo", - "none", - ], - }) - .option("examples", { - type: "array", - string: true, - describe: "Examples to include", - choices: ["todo", "ai", "none"], - }) - .option("git", { - type: "boolean", - describe: "Initialize git repository (use --no-git to skip)", - }) - .option("package-manager", { - alias: "pm", - type: "string", - describe: "Package manager", - choices: ["npm", "pnpm", "bun"], - }) - .option("install", { - type: "boolean", - describe: "Install dependencies (use --no-install to skip)", - }) - .option("db-setup", { - type: "string", - describe: "Database setup", - choices: [ - "turso", - "neon", - "prisma-postgres", - "mongodb-atlas", - "supabase", - "none", - ], - }) - .option("backend", { - type: "string", - describe: "Backend framework", - choices: [ - "hono", - "express", - "fastify", - "next", - "elysia", - "convex", - "none", - ], - }) - .option("runtime", { - type: "string", - describe: "Runtime", - choices: ["bun", "node", "none"], - }) - .option("api", { - type: "string", - describe: "API type", - choices: ["trpc", "orpc", "none"], - }) - .completion() - .recommendCommands() - .version(getLatestCLIVersion()) - .alias("version", "v") - .help() - .alias("help", "h") - .strict() - .wrap(null) - .parse(); - - return argv as YargsArgv; -} diff --git a/apps/cli/src/helpers/project-generation/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts index 65a6405ad..b1b081352 100644 --- a/apps/cli/src/helpers/project-generation/template-manager.ts +++ b/apps/cli/src/helpers/project-generation/template-manager.ts @@ -3,8 +3,8 @@ import fs from "fs-extra"; import { globby } from "globby"; import { PKG_ROOT } from "../../constants"; import type { ProjectConfig } from "../../types"; -import { processTemplate } from "../../utils/template-processor"; import { addPackageDependency } from "../../utils/add-package-deps"; +import { processTemplate } from "../../utils/template-processor"; async function processAndCopyFiles( sourcePattern: string | string[], @@ -77,7 +77,13 @@ export async function setupFrontendTemplates( const _hasNative = hasNativeWind || hasUnistyles; const isConvex = context.backend === "convex"; - if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb || hasAngularWeb) { + if ( + hasReactWeb || + hasNuxtWeb || + hasSvelteWeb || + hasSolidWeb || + hasAngularWeb + ) { const webAppDir = path.join(projectDir, "apps/web"); await fs.ensureDir(webAppDir); @@ -195,7 +201,12 @@ export async function setupFrontendTemplates( `templates/api/${context.api}/web/angular`, ); if (await fs.pathExists(apiWebAngularDir)) { - await processAndCopyFiles("**/*", apiWebAngularDir, webAppDir, context); + await processAndCopyFiles( + "**/*", + apiWebAngularDir, + webAppDir, + context, + ); } else { } } @@ -454,7 +465,11 @@ export async function setupAuthTemplate( } if ( - (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb || hasAngularWeb) && + (hasReactWeb || + hasNuxtWeb || + hasSvelteWeb || + hasSolidWeb || + hasAngularWeb) && webAppDirExists ) { if (hasReactWeb) { @@ -523,9 +538,17 @@ export async function setupAuthTemplate( } } } else if (hasAngularWeb) { - const authWebAngularSrc = path.join(PKG_ROOT, "templates/auth/web/angular"); + const authWebAngularSrc = path.join( + PKG_ROOT, + "templates/auth/web/angular", + ); if (await fs.pathExists(authWebAngularSrc)) { - await processAndCopyFiles("**/*", authWebAngularSrc, webAppDir, context); + await processAndCopyFiles( + "**/*", + authWebAngularSrc, + webAppDir, + context, + ); } else { } } @@ -795,7 +818,12 @@ export async function setupExamplesTemplate( } else if (hasAngularWeb) { const exampleWebAngularSrc = path.join(exampleBaseDir, "web/angular"); if (await fs.pathExists(exampleWebAngularSrc)) { - await processAndCopyFiles("**/*", exampleWebAngularSrc, webAppDir, context); + await processAndCopyFiles( + "**/*", + exampleWebAngularSrc, + webAppDir, + context, + ); } else { } } diff --git a/apps/cli/src/helpers/setup/api-setup.ts b/apps/cli/src/helpers/setup/api-setup.ts index d8c9f3ce9..7438951b2 100644 --- a/apps/cli/src/helpers/setup/api-setup.ts +++ b/apps/cli/src/helpers/setup/api-setup.ts @@ -107,12 +107,9 @@ export async function setupApi(config: ProjectConfig): Promise { }); } } else if (hasAngularWeb) { - if(api === "trpc"){ + if (api === "trpc") { await addPackageDependency({ - dependencies: [ - "@trpc/client", - "@trpc/server", - ], + dependencies: ["@trpc/client", "@trpc/server"], projectDir: webDir, }); } else if (api === "orpc") { @@ -225,17 +222,17 @@ export async function setupApi(config: ProjectConfig): Promise { } } } - if(needsAngularQuery && !isConvex){ - if(webDirExists){ - const webPkgJsonPath = path.join(webDir, "package.json"); - if (await fs.pathExists(webPkgJsonPath)) { - await addPackageDependency({ - dependencies: ["@tanstack/angular-query-experimental"], - projectDir: webDir, - }); - } - } - } + if (needsAngularQuery && !isConvex) { + if (webDirExists) { + const webPkgJsonPath = path.join(webDir, "package.json"); + if (await fs.pathExists(webPkgJsonPath)) { + await addPackageDependency({ + dependencies: ["@tanstack/angular-query-experimental"], + projectDir: webDir, + }); + } + } + } if (isConvex) { if (webDirExists) { const webPkgJsonPath = path.join(webDir, "package.json"); diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts index 0cc1b1bcb..0eddf5b1b 100644 --- a/apps/cli/src/prompts/examples.ts +++ b/apps/cli/src/prompts/examples.ts @@ -38,7 +38,11 @@ export async function getExamplesChoice( }, ]; - if (backend !== "elysia" && !frontends?.includes("solid") && !frontends?.includes("angular")) { + if ( + backend !== "elysia" && + !frontends?.includes("solid") && + !frontends?.includes("angular") + ) { options.push({ value: "ai" as const, label: "AI Chat", diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 72d90bfc3..d1379ffa5 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -31,6 +31,7 @@ export const FrontendSchema = z "native-unistyles", "svelte", "solid", + "angular", "none", ]) .describe("Frontend framework"); diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index 6eb4fcc73..e84340c61 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -558,8 +558,17 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { const isSvelte = nextStack.webFrontend.includes("svelte"); const isSolid = nextStack.webFrontend.includes("solid"); const isAngular = nextStack.webFrontend.includes("angular"); - if ((isNuxt || isSvelte || isSolid || isAngular) && nextStack.api === "trpc") { - const frontendName = isNuxt ? "Nuxt" : isSvelte ? "Svelte" : isSolid ? "Solid" : "Angular"; + if ( + (isNuxt || isSvelte || isSolid || isAngular) && + nextStack.api === "trpc" + ) { + const frontendName = isNuxt + ? "Nuxt" + : isSvelte + ? "Svelte" + : isSolid + ? "Solid" + : "Angular"; notes.api.notes.push( `${frontendName} requires oRPC. It will be selected automatically.`, ); @@ -745,7 +754,8 @@ const getCompatibilityRules = (stack: StackState) => { hasNativeFrontend, hasPWACompatible: hasPWACompatibleFrontend(stack.webFrontend), hasTauriCompatible: hasTauriCompatibleFrontend(stack.webFrontend), - hasNuxtOrSvelteOrSolidOrAngular: hasNuxt || hasSvelte || hasSolid || hasAngular, + hasNuxtOrSvelteOrSolidOrAngular: + hasNuxt || hasSvelte || hasSolid || hasAngular, hasSolid, hasNuxt, hasSvelte,