This commit is contained in:
parent
1fdf7516a5
commit
49e1bcac81
@ -13,43 +13,45 @@
|
|||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<h3 class="text-lg font-semibold text-gray-700 truncate">{{ p.name }}</h3>
|
<h3 class="text-lg font-semibold text-gray-700 truncate">{{ p.name }}</h3>
|
||||||
<span class="text-xs px-2 py-1 rounded-full"
|
<span class="text-xs px-2 py-1 rounded-full"
|
||||||
[ngClass]="p.path ? 'bg-green-100 text-green-600' : 'bg-yellow-100 text-yellow-600'">
|
[ngClass]="!p.active ? 'bg-red-100 text-red-600' :
|
||||||
{{ p.path ? 'Uploaded' : 'Pending Upload' }}
|
p.path ? 'bg-green-100 text-green-600' :
|
||||||
</span>
|
'bg-yellow-100 text-yellow-600'">
|
||||||
|
{{ !p.active ? 'Pending Payment' : (p.path ? 'Uploaded' : 'Pending Upload') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="https://{{ p.domain }}.portfolio-host.com" class=" text-sm text-gray-500 break-words mb-4">{{ p.domain }}.portfolio-host.com</a>
|
<a href="https://{{ p.domain }}" class=" text-sm text-gray-500 break-words mb-4">{{ p.domain }}</a>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4 mt-4">
|
<div class="flex flex-col md:flex-row items-center justify-between gap-4 mt-4">
|
||||||
|
|
||||||
<!-- File Input -->
|
<!-- File Input -->
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
(change)="uploadZip($event, p.id)"
|
(change)="uploadZip($event, p.id)"
|
||||||
class="text-sm text-gray-500 file:mr-4 file:py-2 file:px-4
|
class="text-sm text-gray-500 file:mr-4 file:py-2 file:px-4
|
||||||
file:rounded-full file:border-0
|
file:rounded-full file:border-0
|
||||||
file:text-sm file:font-semibold
|
file:text-sm file:font-semibold
|
||||||
file:bg-blue-50 file:text-blue-700
|
file:bg-blue-50 file:text-blue-700
|
||||||
hover:file:bg-blue-100"
|
hover:file:bg-blue-100"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Deploy Button (conditional) -->
|
<!-- Deploy Button (conditional) -->
|
||||||
@if (p.path) {
|
@if (p.path) {
|
||||||
<button
|
<button
|
||||||
(click)="deploy(p.id)"
|
(click)="deploy(p.id)"
|
||||||
class="inline-flex items-center justify-center rounded-2xl px-3 py-2 text-sm font-medium
|
class="inline-flex items-center justify-center rounded-2xl px-3 py-2 text-sm font-medium
|
||||||
text-white bg-gradient-to-r from-blue-600 to-blue-500 shadow hover:shadow-md
|
text-white bg-gradient-to-r from-blue-600 to-blue-500 shadow hover:shadow-md
|
||||||
transition-all duration-200 hover:from-blue-700 hover:to-blue-600"
|
transition-all duration-200 hover:from-blue-700 hover:to-blue-600"
|
||||||
>
|
>
|
||||||
🚀 Deploy
|
🚀 Deploy
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Uploading Status -->
|
<!-- Uploading Status -->
|
||||||
@if (uploading[p.id]) {
|
@if (uploading[p.id]) {
|
||||||
<p class="text-blue-500 text-sm mt-2 text-center w-full">Uploading...</p>
|
<p class="text-blue-500 text-sm mt-2 text-center w-full">Uploading...</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<div class="min-h-screen bg-gray-50 text-gray-800 font-sans">
|
<div class="min-h-screen bg-gray-50 text-gray-800 font-sans">
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section
|
<section
|
||||||
class="bg-white py-20 px-6 text-center md:text-left md:px-20 flex flex-col md:flex-row items-center justify-between">
|
class="bg-white py-20 px-6 text-center md:text-left md:px-20 flex flex-col flex items-center justify-between">
|
||||||
<div class="max-w-xl">
|
<div class="max-w-xl">
|
||||||
<h1 class="text-4xl md:text-6xl font-bold leading-tight mb-4">
|
<h1 class="text-4xl md:text-6xl font-bold leading-tight mb-4">
|
||||||
Host Your Portfolio. <br class="hidden md:block"> Impress the World.
|
Host Your Portfolio. <br class="hidden md:block"> Impress the World.
|
||||||
@ -13,15 +13,13 @@
|
|||||||
<button (click)=signUp() class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-xl shadow">
|
<button (click)=signUp() class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-xl shadow">
|
||||||
Start Free Trial
|
Start Free Trial
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button (click)=exemple()
|
||||||
class="border border-gray-300 hover:border-blue-600 text-blue-600 hover:text-blue-700 font-semibold py-2 px-4 rounded-xl">
|
class="border border-gray-300 hover:border-blue-600 text-blue-600 hover:text-blue-700 font-semibold py-2 px-4 rounded-xl">
|
||||||
See Examples
|
See Examples
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-12 md:mt-0 md:ml-12 w-full md:w-1/2">
|
|
||||||
<img alt="Portfolio Illustration" class="w-full h-auto" src="assets/hero-illustration.svg">
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Features Section -->
|
<!-- Features Section -->
|
||||||
@ -29,26 +27,21 @@
|
|||||||
<div class="text-center mb-12">
|
<div class="text-center mb-12">
|
||||||
<h2 class="text-3xl md:text-4xl font-bold">Why Choose PortfolioHost?</h2>
|
<h2 class="text-3xl md:text-4xl font-bold">Why Choose PortfolioHost?</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-10 md:grid-cols-4 text-center">
|
<div class="flex justify-center gap-10 md:grid-cols-4 text-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-4xl mb-4">⚡</div>
|
<div class="text-3xl mb-3">⚡</div>
|
||||||
<h3 class="font-semibold text-xl mb-2">Blazing Fast Hosting</h3>
|
<h3 class="font-semibold text-xl mb-2">Blazing Fast Hosting</h3>
|
||||||
<p class="text-gray-600">NVMe servers & global CDN ensure speed and uptime.</p>
|
<p class="text-gray-600">NVMe servers & lightning fast deployment pipeline.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-4xl mb-4">🌐</div>
|
<div class="text-3xl mb-3">🌐</div>
|
||||||
<h3 class="font-semibold text-xl mb-2">Custom Domains</h3>
|
<h3 class="font-semibold text-xl mb-2">Free Domains</h3>
|
||||||
<p class="text-gray-600">Use your own domain or get a .portfoliohost.dev address.</p>
|
<p class="text-gray-600">Get a free .portfolio-host.com address.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-4xl mb-4">🚀</div>
|
<div class="text-3xl mb-3">🚀</div>
|
||||||
<h3 class="font-semibold text-xl mb-2">1-Click Deploy</h3>
|
<h3 class="font-semibold text-xl mb-2">1-Click Deploy</h3>
|
||||||
<p class="text-gray-600">Integrate with GitHub or push manually in seconds.</p>
|
<p class="text-gray-600">Integrate with GitHub (COMING SOON) or push manually in seconds.</p>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-4xl mb-4">📱</div>
|
|
||||||
<h3 class="font-semibold text-xl mb-2">Fully Responsive</h3>
|
|
||||||
<p class="text-gray-600">Mobile-first designs that look great everywhere.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -56,7 +49,7 @@
|
|||||||
<!-- Final CTA -->
|
<!-- Final CTA -->
|
||||||
<section class="bg-blue-600 py-16 px-6 md:px-20 text-white text-center">
|
<section class="bg-blue-600 py-16 px-6 md:px-20 text-white text-center">
|
||||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">Ready to impress recruiters and clients?</h2>
|
<h2 class="text-3xl md:text-4xl font-bold mb-4">Ready to impress recruiters and clients?</h2>
|
||||||
<p class="mb-6 text-lg">Create your portfolio in minutes — no coding required.</p>
|
<p class="mb-6 text-lg">Host your portfolio in seconds.</p>
|
||||||
<button class="bg-white text-blue-600 font-bold py-2 px-6 rounded-xl shadow hover:bg-gray-100">
|
<button class="bg-white text-blue-600 font-bold py-2 px-6 rounded-xl shadow hover:bg-gray-100">
|
||||||
Start My Portfolio Now
|
Start My Portfolio Now
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {ApiService} from '../../services/api';
|
import {ApiService} from '../../services/api';
|
||||||
|
import {PortfolioService} from '../../services/portfolio.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -8,14 +9,24 @@ import {ApiService} from '../../services/api';
|
|||||||
})
|
})
|
||||||
export class LandingComponent {
|
export class LandingComponent {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private router: Router,
|
private portfolioService: PortfolioService,
|
||||||
) {
|
private router: Router,
|
||||||
}
|
) {
|
||||||
|
|
||||||
signUp(): void{
|
|
||||||
this.router.navigate(['/register']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signUp(): void {
|
||||||
|
this.router.navigate(['/register']);
|
||||||
|
}
|
||||||
|
|
||||||
|
exemple() {
|
||||||
|
this.portfolioService.getRandomPortfolio().subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
window.location.href = 'https://' + res.data.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export interface Portfolio {
|
export interface Portfolio {
|
||||||
|
active: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
import {HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
|
||||||
import { Observable, throwError, BehaviorSubject } from 'rxjs';
|
import {BehaviorSubject, Observable, throwError} from 'rxjs';
|
||||||
import { catchError, tap } from 'rxjs/operators';
|
import {catchError, tap} from 'rxjs/operators';
|
||||||
import { environmentProd as env } from '../../environments/environment' ;
|
import {environmentProd as env} from '../../environments/environment';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
|
||||||
export interface LoginCredentials {
|
export interface LoginCredentials {
|
||||||
email: string;
|
email: string;
|
||||||
@ -37,32 +38,7 @@ export class ApiService {
|
|||||||
private tokenSubject = new BehaviorSubject<string | null>(this.getStoredToken());
|
private tokenSubject = new BehaviorSubject<string | null>(this.getStoredToken());
|
||||||
public token$ = this.tokenSubject.asObservable();
|
public token$ = this.tokenSubject.asObservable();
|
||||||
|
|
||||||
constructor(protected http: HttpClient) {}
|
constructor(protected http: HttpClient, private router: Router) {}
|
||||||
|
|
||||||
// Get stored token from localStorage
|
|
||||||
private getStoredToken(): string | null {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
return localStorage.getItem('auth_token');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set token in localStorage and update subject
|
|
||||||
private setToken(token: string): void {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.setItem('auth_token', token);
|
|
||||||
}
|
|
||||||
this.tokenSubject.next(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove token from localStorage and update subject
|
|
||||||
private removeToken(): void {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
console.log('?? ?? ??')
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
}
|
|
||||||
this.tokenSubject.next(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current token value
|
// Get current token value
|
||||||
get currentToken(): string | null {
|
get currentToken(): string | null {
|
||||||
@ -74,79 +50,6 @@ export class ApiService {
|
|||||||
return !!this.currentToken;
|
return !!this.currentToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get HTTP headers with auth token
|
|
||||||
private getHeaders(): HttpHeaders {
|
|
||||||
let headers = new HttpHeaders({
|
|
||||||
//'Content-Type': 'application/json',
|
|
||||||
//'Accept': 'application/json'
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = this.currentToken;
|
|
||||||
if (token) {
|
|
||||||
headers = headers.set('Authorization', `Bearer ${token}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle HTTP errors
|
|
||||||
private handleError(error: HttpErrorResponse): Observable<never> {
|
|
||||||
let errorMessage = 'An unknown error occurred';
|
|
||||||
|
|
||||||
if (error.error instanceof ErrorEvent) {
|
|
||||||
// Client-side error
|
|
||||||
errorMessage = error.error.message;
|
|
||||||
} else {
|
|
||||||
// Server-side error
|
|
||||||
if (error.status === 401) {
|
|
||||||
//this.removeToken();
|
|
||||||
errorMessage = 'Unauthorized. Please login again.';
|
|
||||||
} else if (error.status === 422) {
|
|
||||||
errorMessage = 'Validation error';
|
|
||||||
} else if (error.error?.message) {
|
|
||||||
errorMessage = error.error.message;
|
|
||||||
} else {
|
|
||||||
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('API Error:', error);
|
|
||||||
return throwError(() => ({ ...error, userMessage: errorMessage }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic HTTP methods
|
|
||||||
protected get<T>(endpoint: string): Observable<T> {
|
|
||||||
return this.http.get<T>(`${this.baseUrl}${endpoint}`, {
|
|
||||||
headers: this.getHeaders()
|
|
||||||
}).pipe(
|
|
||||||
catchError(this.handleError.bind(this))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected post<T>(endpoint: string, data: any): Observable<T> {
|
|
||||||
return this.http.post<T>(`${this.baseUrl}${endpoint}`, data, {
|
|
||||||
headers: this.getHeaders()
|
|
||||||
}).pipe(
|
|
||||||
catchError(this.handleError.bind(this))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private put<T>(endpoint: string, data: any): Observable<T> {
|
|
||||||
return this.http.put<T>(`${this.baseUrl}${endpoint}`, data, {
|
|
||||||
headers: this.getHeaders()
|
|
||||||
}).pipe(
|
|
||||||
catchError(this.handleError.bind(this))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private delete<T>(endpoint: string): Observable<T> {
|
|
||||||
return this.http.delete<T>(`${this.baseUrl}${endpoint}`, {
|
|
||||||
headers: this.getHeaders()
|
|
||||||
}).pipe(
|
|
||||||
catchError(this.handleError.bind(this))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentication methods
|
// Authentication methods
|
||||||
register(userData: RegisterData): Observable<ApiResponse<AuthResponse>> {
|
register(userData: RegisterData): Observable<ApiResponse<AuthResponse>> {
|
||||||
return this.post<ApiResponse<AuthResponse>>('/auth/register', userData).pipe(
|
return this.post<ApiResponse<AuthResponse>>('/auth/register', userData).pipe(
|
||||||
@ -207,4 +110,102 @@ export class ApiService {
|
|||||||
deleteData<T>(endpoint: string): Observable<T> {
|
deleteData<T>(endpoint: string): Observable<T> {
|
||||||
return this.delete<T>(endpoint);
|
return this.delete<T>(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic HTTP methods
|
||||||
|
protected get<T>(endpoint: string): Observable<T> {
|
||||||
|
return this.http.get<T>(`${this.baseUrl}${endpoint}`, {
|
||||||
|
headers: this.getHeaders()
|
||||||
|
}).pipe(
|
||||||
|
catchError(this.handleError.bind(this))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected post<T>(endpoint: string, data: any): Observable<T> {
|
||||||
|
return this.http.post<T>(`${this.baseUrl}${endpoint}`, data, {
|
||||||
|
headers: this.getHeaders()
|
||||||
|
}).pipe(
|
||||||
|
catchError(this.handleError.bind(this))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stored token from localStorage
|
||||||
|
private getStoredToken(): string | null {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return localStorage.getItem('auth_token');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set token in localStorage and update subject
|
||||||
|
private setToken(token: string): void {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('auth_token', token);
|
||||||
|
}
|
||||||
|
this.tokenSubject.next(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove token from localStorage and update subject
|
||||||
|
private removeToken(): void {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('?? ?? ??')
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
}
|
||||||
|
this.tokenSubject.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get HTTP headers with auth token
|
||||||
|
private getHeaders(): HttpHeaders {
|
||||||
|
let headers = new HttpHeaders({
|
||||||
|
//'Content-Type': 'application/json',
|
||||||
|
//'Accept': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = this.currentToken;
|
||||||
|
if (token) {
|
||||||
|
headers = headers.set('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle HTTP errors
|
||||||
|
private handleError(error: HttpErrorResponse): Observable<never> {
|
||||||
|
let errorMessage = 'An unknown error occurred';
|
||||||
|
|
||||||
|
if (error.error instanceof ErrorEvent) {
|
||||||
|
// Client-side error
|
||||||
|
errorMessage = error.error.message;
|
||||||
|
} else {
|
||||||
|
// Server-side error
|
||||||
|
if (error.status === 401) {
|
||||||
|
this.removeToken();
|
||||||
|
this.router.navigate(['/login'])
|
||||||
|
} else if (error.status === 422) {
|
||||||
|
errorMessage = 'Validation error';
|
||||||
|
} else if (error.error?.message) {
|
||||||
|
errorMessage = error.error.message;
|
||||||
|
} else {
|
||||||
|
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('API Error:', error);
|
||||||
|
return throwError(() => ({...error, userMessage: errorMessage}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private put<T>(endpoint: string, data: any): Observable<T> {
|
||||||
|
return this.http.put<T>(`${this.baseUrl}${endpoint}`, data, {
|
||||||
|
headers: this.getHeaders()
|
||||||
|
}).pipe(
|
||||||
|
catchError(this.handleError.bind(this))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private delete<T>(endpoint: string): Observable<T> {
|
||||||
|
return this.http.delete<T>(`${this.baseUrl}${endpoint}`, {
|
||||||
|
headers: this.getHeaders()
|
||||||
|
}).pipe(
|
||||||
|
catchError(this.handleError.bind(this))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,4 +27,9 @@ export class PortfolioService extends ApiService{
|
|||||||
console.log('Deploying portfolio with ID:', portfolioId);
|
console.log('Deploying portfolio with ID:', portfolioId);
|
||||||
return this.http.post<ApiResponse>(this.baseUrl + `/portfolios/${portfolioId}/deploy`, {});
|
return this.http.post<ApiResponse>(this.baseUrl + `/portfolios/${portfolioId}/deploy`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRandomPortfolio() : Observable<ApiResponse>
|
||||||
|
{
|
||||||
|
return this.http.get<ApiResponse>(this.baseUrl + `/portfolio/random`, {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user