This commit is contained in:
Alexis Bruneteau 2025-05-05 00:14:47 +02:00
parent bdb1fca186
commit 42a2f89ae3
19 changed files with 1456 additions and 118 deletions

View File

@ -99,7 +99,8 @@
"buildTarget": "host-one-euro:build:development"
}
},
"defaultConfiguration": "development"
"defaultConfiguration": "development",
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,4 @@
export default {
plugins: ["@tailwindcss/postcss"],
};

View File

@ -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 {}

View File

@ -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 },
];

View 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');
});
}
}

View 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">
Yesadd a CNAME to <code>yoursite.host-1euro.com</code> and well
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&nbsp;days free; no credit card required.
</p>
</details>
</div>
</section>
`,
})
export class FaqComponent {}

View File

@ -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 Lets 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.' },
];
}

View 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 {}

View 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 {}

View File

@ -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 {}

View 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 {}

View 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();
}

View 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 {}

View File

@ -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);

View File

@ -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; }

View File

@ -0,0 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,ts}'], // keep this line
theme: { extend: {} },
plugins: [],
};