login
This commit is contained in:
parent
bdb1fca186
commit
42a2f89ae3
@ -99,7 +99,8 @@
|
||||
"buildTarget": "host-one-euro:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
"defaultConfiguration": "development",
|
||||
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
|
||||
1131
frontend/package-lock.json
generated
1131
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@
|
||||
"@angular/platform-server": "^19.2.0",
|
||||
"@angular/router": "^19.2.0",
|
||||
"@angular/ssr": "^19.2.10",
|
||||
"@tailwindcss/cli": "^4.1.5",
|
||||
"express": "^4.18.2",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
@ -30,6 +31,9 @@
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^18.18.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
4
frontend/postcss.config.mjs
Normal file
4
frontend/postcss.config.mjs
Normal file
@ -0,0 +1,4 @@
|
||||
export default {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
import { NavbarComponent } from './shared/layout/navbar/navbar.component';
|
||||
import { FooterComponent } from './shared/layout/footer/footer.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
standalone: true,
|
||||
imports: [NavbarComponent, FooterComponent, RouterOutlet],
|
||||
template: `
|
||||
<h1>Welcome to {{title}}!</h1>
|
||||
<app-navbar />
|
||||
|
||||
<router-outlet />
|
||||
<main class="min-h-[calc(100vh-7rem)]">
|
||||
<router-outlet />
|
||||
</main>
|
||||
|
||||
<app-footer />
|
||||
`,
|
||||
styles: [],
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'host-one-euro';
|
||||
}
|
||||
export class AppComponent {}
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { LoginComponent } from './pages/auth/login/login.component';
|
||||
import { HomeComponent } from './pages/landing/home/home.component';
|
||||
import {Routes} from "@angular/router";
|
||||
|
||||
export const routes: Routes = [];
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: HomeComponent }, // landing
|
||||
{ path: 'login', component: LoginComponent }, // ← new
|
||||
// { path: 'signup', component: SignupComponent },
|
||||
];
|
||||
140
frontend/src/app/pages/auth/login/login.component.ts
Normal file
140
frontend/src/app/pages/auth/login/login.component.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import {
|
||||
ReactiveFormsModule,
|
||||
FormBuilder,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { throwError } from 'rxjs';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, RouterLink],
|
||||
template: `
|
||||
<section
|
||||
class="flex min-h-screen items-center justify-center bg-slate-50 px-4">
|
||||
<form
|
||||
[formGroup]="form"
|
||||
(ngSubmit)="submit()"
|
||||
class="w-full max-w-md space-y-6 rounded-xl bg-white p-8 shadow-lg">
|
||||
<h1 class="text-center text-2xl font-semibold">Log in</h1>
|
||||
|
||||
<!-- email -->
|
||||
<div>
|
||||
<label for="email" class="mb-1 block font-medium">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
formControlName="email"
|
||||
autocomplete="email"
|
||||
required
|
||||
class="w-full rounded-lg border px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
/>
|
||||
<p
|
||||
*ngIf="
|
||||
form.controls.email.invalid && form.controls.email.touched
|
||||
"
|
||||
class="mt-1 text-xs text-red-600">
|
||||
Enter a valid email.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- password -->
|
||||
<div>
|
||||
<label for="password" class="mb-1 block font-medium">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
formControlName="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
class="w-full rounded-lg border px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
/>
|
||||
<p
|
||||
*ngIf="
|
||||
form.controls.password.invalid && form.controls.password.touched
|
||||
"
|
||||
class="mt-1 text-xs text-red-600">
|
||||
Password is required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- remember + error -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
formControlName="remember"
|
||||
class="rounded border-slate-300 text-sky-600 focus:ring-sky-500" />
|
||||
Remember me
|
||||
</label>
|
||||
|
||||
<a routerLink="/forgot" class="text-xs text-sky-600 hover:underline"
|
||||
>Forgot?</a
|
||||
>
|
||||
</div>
|
||||
<p *ngIf="error" class="text-sm text-red-600">{{ error }}</p>
|
||||
|
||||
<!-- submit -->
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="form.invalid || loading"
|
||||
class="w-full rounded-full bg-sky-500 px-6 py-2 font-semibold text-white transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60">
|
||||
{{ loading ? 'Signing in…' : 'Sign in' }}
|
||||
</button>
|
||||
|
||||
<p class="text-center text-xs">
|
||||
No account?
|
||||
<a routerLink="/signup" class="text-sky-600 hover:underline"
|
||||
>Create one</a
|
||||
>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class LoginComponent {
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
private readonly fb = inject(FormBuilder);
|
||||
|
||||
readonly form = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', Validators.required],
|
||||
remember: false,
|
||||
});
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
submit() {
|
||||
if (this.form.invalid || this.loading) return;
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
this.http
|
||||
.post<{ token: string }>('/api/login', this.form.value)
|
||||
.pipe(
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
this.error =
|
||||
err.status === 401
|
||||
? 'Invalid credentials.'
|
||||
: 'Login failed, please try again.';
|
||||
this.loading = false;
|
||||
return throwError(() => err);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.loading = false;
|
||||
// TODO: store token in localStorage/cookie
|
||||
this.router.navigateByUrl('/dashboard');
|
||||
});
|
||||
}
|
||||
}
|
||||
33
frontend/src/app/pages/landing/faq/faq.component.ts
Normal file
33
frontend/src/app/pages/landing/faq/faq.component.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-faq',
|
||||
standalone: true,
|
||||
template: `
|
||||
<section class="py-16">
|
||||
<h2 class="mb-8 text-center text-2xl font-bold">FAQ</h2>
|
||||
|
||||
<div class="mx-auto max-w-2xl space-y-4">
|
||||
<details class="rounded-lg bg-slate-50 p-4">
|
||||
<summary class="cursor-pointer font-medium">
|
||||
Can I use my own domain?
|
||||
</summary>
|
||||
<p class="mt-2 text-sm text-slate-600">
|
||||
Yes—add a CNAME to <code>yoursite.host-1euro.com</code> and we’ll
|
||||
provision HTTPS automatically.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details class="rounded-lg bg-slate-50 p-4">
|
||||
<summary class="cursor-pointer font-medium">
|
||||
Is there a free trial?
|
||||
</summary>
|
||||
<p class="mt-2 text-sm text-slate-600">
|
||||
You get 7 days free; no credit card required.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class FaqComponent {}
|
||||
@ -0,0 +1,35 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { NgFor } from '@angular/common';
|
||||
|
||||
interface Feature {
|
||||
icon: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-feature-grid',
|
||||
standalone: true,
|
||||
imports: [NgFor],
|
||||
template: `
|
||||
<section id="features" class="py-16">
|
||||
<div class="mx-auto grid max-w-6xl gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<article *ngFor="let f of features" class="space-y-3">
|
||||
<div class="text-2xl">{{ f.icon }}</div>
|
||||
<h3 class="text-lg font-semibold">{{ f.title }}</h3>
|
||||
<p class="text-sm text-slate-600">{{ f.text }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class FeatureGridComponent {
|
||||
protected readonly features: Feature[] = [
|
||||
{ icon: '🚀', title: 'Fast CDN', text: 'Edge-cached assets worldwide.' },
|
||||
{ icon: '🔒', title: 'Free HTTPS', text: 'Auto-renewed Let’s Encrypt.' },
|
||||
{ icon: '♻️', title: '1-click deploy', text: 'Push to Git, live in 30 s.' },
|
||||
{ icon: '📈', title: 'Analytics', text: 'See traffic & referrers.' },
|
||||
{ icon: '📦', title: 'Daily backups', text: 'Off-node, encrypted.' },
|
||||
{ icon: '🛡', title: 'DDoS guard', text: 'Layer-7 filtering.' },
|
||||
];
|
||||
}
|
||||
36
frontend/src/app/pages/landing/hero/hero.component.ts
Normal file
36
frontend/src/app/pages/landing/hero/hero.component.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero',
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
template: `
|
||||
<section class="grid gap-10 py-24 md:grid-cols-2">
|
||||
<div class="place-self-center text-center md:text-left">
|
||||
<h1 class="max-w-xl text-4xl font-bold md:text-5xl">
|
||||
Host your portfolio for
|
||||
<span class="text-sky-500">1 € / month</span>
|
||||
</h1>
|
||||
<p class="mx-auto mt-4 max-w-prose text-slate-600 md:mx-0">
|
||||
Lightning-fast global edge, free HTTPS, zero-click deploys.
|
||||
</p>
|
||||
|
||||
<a
|
||||
routerLink="/signup"
|
||||
class="mt-8 inline-block rounded-full bg-sky-500 px-8 py-3 font-semibold text-white transition hover:scale-105">
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<figure
|
||||
class="relative aspect-video w-full overflow-hidden rounded-xl shadow-lg">
|
||||
<img
|
||||
src="assets/demo-dashboard.png"
|
||||
alt="Dashboard preview"
|
||||
class="absolute inset-0 h-full w-full object-cover" />
|
||||
</figure>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class HeroComponent {}
|
||||
26
frontend/src/app/pages/landing/home/home.component.ts
Normal file
26
frontend/src/app/pages/landing/home/home.component.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HeroComponent } from '../hero/hero.component';
|
||||
import { LogoStripComponent } from '../logo-strip/logo-strip.component';
|
||||
import { FeatureGridComponent } from '../feature-grid/feature-grid.component';
|
||||
import { PricingComponent } from '../pricing/pricing.component';
|
||||
import { FaqComponent } from '../faq/faq.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [
|
||||
HeroComponent,
|
||||
LogoStripComponent,
|
||||
FeatureGridComponent,
|
||||
PricingComponent,
|
||||
FaqComponent,
|
||||
],
|
||||
template: `
|
||||
<app-hero />
|
||||
<app-logo-strip />
|
||||
<app-feature-grid />
|
||||
<app-pricing />
|
||||
<app-faq />
|
||||
`,
|
||||
})
|
||||
export class HomeComponent {}
|
||||
@ -0,0 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logo-strip',
|
||||
standalone: true,
|
||||
template: `
|
||||
<section class="bg-slate-50 py-6">
|
||||
<p class="mb-4 text-center text-sm text-slate-500">
|
||||
Trusted by 300+ creators
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center justify-center gap-8">
|
||||
<img src="assets/logos/figma.svg" class="h-6 opacity-70" />
|
||||
<img src="assets/logos/vue.svg" class="h-6 opacity-70" />
|
||||
<img src="assets/logos/react.svg" class="h-6 opacity-70" />
|
||||
<img src="assets/logos/aws.svg" class="h-6 opacity-70" />
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class LogoStripComponent {}
|
||||
31
frontend/src/app/pages/landing/pricing/pricing.component.ts
Normal file
31
frontend/src/app/pages/landing/pricing/pricing.component.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pricing',
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
template: `
|
||||
<section id="pricing" class="bg-slate-50 py-20">
|
||||
<div
|
||||
class="mx-auto max-w-sm rounded-2xl bg-white p-10 text-center shadow-lg ring-1 ring-slate-200">
|
||||
<h2 class="mb-2 text-xl font-semibold">Portfolio tier</h2>
|
||||
<p class="text-4xl font-bold text-sky-500">1 €</p>
|
||||
<p class="mb-6 text-sm text-slate-500">per month</p>
|
||||
|
||||
<ul class="mb-8 space-y-2 text-left text-sm">
|
||||
<li>• 500 MB disk</li>
|
||||
<li>• 10 GB bandwidth</li>
|
||||
<li>• 24/7 chat support</li>
|
||||
</ul>
|
||||
|
||||
<a
|
||||
routerLink="/signup"
|
||||
class="inline-block rounded-full bg-sky-500 px-8 py-3 font-semibold text-white transition hover:scale-105">
|
||||
Start free trial
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class PricingComponent {}
|
||||
23
frontend/src/app/shared/layout/footer/footer.component.ts
Normal file
23
frontend/src/app/shared/layout/footer/footer.component.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
template: `
|
||||
<footer class="bg-slate-900 py-12 text-center text-slate-100">
|
||||
<p class="mb-4 space-x-4 text-sm">
|
||||
<a routerLink="/terms" class="hover:underline">Terms</a>
|
||||
<a routerLink="/privacy" class="hover:underline">Privacy</a>
|
||||
<a routerLink="/status" class="hover:underline">Status</a>
|
||||
</p>
|
||||
<p class="text-xs text-slate-400">
|
||||
© {{ year }} host-1€
|
||||
</p>
|
||||
</footer>
|
||||
`,
|
||||
})
|
||||
export class FooterComponent {
|
||||
readonly year = new Date().getFullYear();
|
||||
}
|
||||
21
frontend/src/app/shared/layout/navbar/navbar.component.ts
Normal file
21
frontend/src/app/shared/layout/navbar/navbar.component.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
template: `
|
||||
<nav
|
||||
class="sticky top-0 z-50 flex h-14 items-center justify-between bg-white/90 px-6 shadow-sm backdrop-blur">
|
||||
<a routerLink="/" class="text-lg font-bold text-sky-500">host-1€</a>
|
||||
|
||||
<ul class="hidden gap-6 md:flex">
|
||||
<li><a href="#features" class="hover:text-sky-500">Features</a></li>
|
||||
<li><a href="#pricing" class="hover:text-sky-500">Pricing</a></li>
|
||||
<li><a routerLink="/login" class="hover:text-sky-500">Login</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
`,
|
||||
})
|
||||
export class NavbarComponent {}
|
||||
@ -1,6 +1,13 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
import { AppComponent } from './app/app.component';
|
||||
import {routes} from "./app/app.routes";
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
provideHttpClient(),
|
||||
],
|
||||
}).catch(console.error);
|
||||
@ -1 +1,8 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
/* Tailwind v3 setup — works inside .scss */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* your own tweaks below … */
|
||||
body { font-family: system-ui, sans-serif; }
|
||||
|
||||
|
||||
6
frontend/tailwind.config.js
Normal file
6
frontend/tailwind.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,ts}'], // keep this line
|
||||
theme: { extend: {} },
|
||||
plugins: [],
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user