Dalam blog ini, kami akan membimbing anda melalui proses mencipta aplikasi pemendek URL menggunakan Angular untuk bahagian hadapan dan CSS Tailwind untuk penggayaan. Pemendek URL ialah alat berguna yang menukar URL yang panjang kepada pautan yang lebih pendek dan lebih terurus. Projek ini akan membantu anda memahami cara membina aplikasi web yang berfungsi dan menyenangkan dari segi estetika menggunakan teknologi pembangunan web moden.
Untuk mengikuti tutorial ini, anda harus mempunyai pemahaman asas tentang Sudut dan sedikit kebiasaan dengan CSS Tailwind. Pastikan anda memasang Node.js dan Angular CLI pada mesin anda.
Mula-mula, buat projek Angular baharu dengan menjalankan arahan berikut dalam terminal anda:
ng new url-shortener-app cd url-shortener-app
Seterusnya, sediakan Tailwind CSS dalam projek Angular anda. Pasang Tailwind CSS dan kebergantungannya melalui npm:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init
Konfigurasikan CSS Tailwind dengan mengemas kini fail tailwind.config.js:
module.exports = { content: [ "./src/**/*.{html,ts}", ], theme: { extend: {}, }, plugins: [], }
Tambahkan arahan Tailwind pada fail src/styles.scss anda:
@tailwind base; @tailwind components; @tailwind utilities;
Buat model URL untuk menentukan struktur data URL. Tambah fail baharu src/app/models/url.model.ts:
export type Urls = Url[]; export interface Url { _id: string; originalUrl: string; shortUrl: string; clicks: number; expirationDate: string; createdAt: string; __v: number; }
Buat perkhidmatan untuk mengendalikan panggilan API yang berkaitan dengan pemendekan URL. Tambah fail baharu src/app/services/url.service.ts:
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Url, Urls } from '../models/url.model'; import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UrlService { private apiUrl = environment.apiUrl; constructor(private http: HttpClient) {} shortenUrl(originalUrl: string): Observable<Url> { return this.http.post<Url>(`${this.apiUrl}/shorten`, { originalUrl }); } getAllUrls(): Observable<Urls> { return this.http.get<Urls>(`${this.apiUrl}/urls`); } getDetails(id: string): Observable<Url> { return this.http.get<Url>(`${this.apiUrl}/details/${id}`); } deleteUrl(id: string): Observable<Url> { return this.http.delete<Url>(`${this.apiUrl}/delete/${id}`); } }
Jana komponen baharu untuk memendekkan URL:
ng generate component shorten
Kemas kini HTML komponen (src/app/shorten/shorten.component.html) kepada seperti yang ditunjukkan di bawah:
<div class="max-w-md mx-auto p-4 shadow-lg rounded-lg mt-4"> <h2 class="text-2xl font-bold mb-2">URL Shortener</h2> <form [formGroup]="urlForm" (ngSubmit)="shortenUrl()"> <div class="flex items-center mb-2"> <input class="flex-1 p-2 border border-gray-300 rounded mr-4" formControlName="originalUrl" placeholder="Enter your URL" required /> <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" type="submit"> Shorten </button> </div> @if (urlForm.get('originalUrl')?.invalid && (urlForm.get('originalUrl')?.dirty || urlForm.get('originalUrl')?.touched)) { <div class="text-red-500" role="alert" aria-live="assertive"> @if (urlForm.get('originalUrl')?.errors?.['required']) { URL is required. } @if (urlForm.get('originalUrl')?.errors?.['pattern']) { Invalid URL format. Please enter a valid URL starting with http:// or https://. } </div> } </form> @if (errorMsg) { <div class="p-4 bg-red-100 rounded mt-4"> <p class="text-red-500">{{ errorMsg }}</p> </div> } @if (shortUrl) { <div class="p-4 bg-green-100 rounded"> <p>Shortened URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + shortUrl" target="_blank">{{ shortUrl }}</a> <button class="ml-2 px-2 py-1 bg-gray-200 text-gray-800 border border-slate-950 rounded hover:bg-gray-300" (click)="copyUrl(redirectUrl + shortUrl)">Copy</button> @if (copyMessage) { <span class="text-green ml-2">{{ copyMessage }}</span> } </p> </div> } </div> <div class="max-w-md mx-auto mt-4 p-2"> <h2 class="text-2xl font-bold mb-4">All URLs</h2> @if (isloading) { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> Loading... </div> </div> } @else if (error) { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> <p class="text-red-500">{{ error }}</p> </div> </div> } @else { @if (urls.length > 0 && !isloading && !error) { <ul> @for (url of urls; track $index) { <li class="p-2 border border-gray-300 rounded mb-2"> <div class="flex justify-between items-center"> <div> URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + url.shortUrl" target="_blank">{{ url.shortUrl }}</a> </div> <div class="flex justify-between items-center"> <button class="px-2 py-1 bg-blue-200 text-blue-800 rounded hover:bg-blue-300" (click)="showDetails(url.shortUrl)">Details</button> <button class="ml-2 px-2 py-1 bg-gray-200 text-gray-800 rounded hover:bg-gray-300" (click)="copyListUrl(redirectUrl + url.shortUrl, $index)">{{ copyIndex === $index ? 'Copied' : 'Copy' }}</button> <button class="ml-2 px-2 py-1 bg-red-200 text-red-800 rounded hover:bg-red-300" (click)="prepareDelete(url.shortUrl)">Delete</button> </div> </div> </li> } </ul> } @else { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> No URLs found. </div> </div> } } </div> @if (showDeleteModal) { <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-4 rounded shadow-lg"> <h3 class="text-xl font-bold mb-2">Confirm Deletion</h3> <p class="mb-4">Are you sure you want to delete this URL?</p> <button class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" (click)="confirmDelete()">Yes, Delete</button> <button class="px-4 py-2 bg-gray-300 text-gray-800 rounded hover:bg-gray-400 ml-2" (click)="showDeleteModal = false">Cancel</button> </div> </div> } @if (showDetailsModal) { <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-4 rounded shadow-lg"> <h3 class="text-xl font-bold mb-2">URL Details</h3> @if (isLoading) { <p class="mb-4">Loading...</p> } @else { <p class="mb-4">Short URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + selectedUrl.shortUrl" target="_blank">{{ selectedUrl.shortUrl }}</a></p> <p class="mb-4">Original URL: <a class="text-blue-500 hover:text-blue-600" [href]="selectedUrl.originalUrl" target="_blank">{{ selectedUrl.originalUrl }}</a></p> <p class="mb-4">Clicks: <span class="text-green-500">{{ selectedUrl.clicks }}</span></p> <p class="mb-4">Created At: {{ selectedUrl.createdAt | date: 'medium' }}</p> <p class="mb-4">Expires At: {{ selectedUrl.expirationDate | date: 'medium' }}</p> <button class="px-4 py-2 bg-gray-300 text-gray-800 rounded hover:bg-gray-400" (click)="showDetailsModal = false">Close</button> } </div> </div> }
Kemas kini fail TypeScript komponen (src/app/shorten/shorten.component.ts) untuk mengendalikan penyerahan borang dan interaksi API:
import { Component, inject, OnInit } from '@angular/core'; import { UrlService } from '../services/url.service'; import { FormControl, FormGroup, ReactiveFormsModule, Validators, } from '@angular/forms'; import { Url } from '../models/url.model'; import { environment } from '../../environments/environment'; import { DatePipe } from '@angular/common'; import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-shorten', standalone: true, imports: [DatePipe, ReactiveFormsModule], templateUrl: './shorten.component.html', styleUrl: './shorten.component.scss', }) export class ShortenComponent implements OnInit { shortUrl: string = ''; redirectUrl = environment.apiUrl + '/'; copyMessage: string = ''; copyListMessage: string = ''; urls: Url[] = []; showDeleteModal = false; showDetailsModal = false; urlToDelete = ''; copyIndex: number = -1; selectedUrl: Url = {} as Url; isLoading = false; isloading = false; error: string = ''; errorMsg: string = ''; urlForm: FormGroup = new FormGroup({}); private unsubscribe$: Subject<void> = new Subject<void>(); urlService = inject(UrlService); ngOnInit() { this.urlForm = new FormGroup({ originalUrl: new FormControl('', [ Validators.required, Validators.pattern('^(http|https)://.*$'), ]), }); this.getAllUrls(); } shortenUrl() { if (this.urlForm.valid) { this.urlService.shortenUrl(this.urlForm.value.originalUrl).pipe(takeUntil(this.unsubscribe$)).subscribe({ next: (response) => { // console.log('Shortened URL: ', response); this.shortUrl = response.shortUrl; this.getAllUrls(); }, error: (error) => { console.error('Error shortening URL: ', error); this.errorMsg = error?.error?.message || 'An error occurred!'; }, }); } } getAllUrls() { this.isloading = true; this.urlService.getAllUrls().pipe(takeUntil(this.unsubscribe$)).subscribe({ next: (response) => { // console.log('All URLs: ', response); this.urls = response; this.isloading = false; }, error: (error) => { console.error('Error getting all URLs: ', error); this.isloading = false; this.error = error?.error?.message || 'An error occurred!'; }, }); } showDetails(id: string) { this.showDetailsModal = true; this.getDetails(id); } getDetails(id: string) { this.isLoading = true; this.urlService.getDetails(id).subscribe({ next: (response) => { // console.log('URL Details: ', response); this.selectedUrl = response; this.isLoading = false; }, error: (error) => { console.error('Error getting URL details: ', error); this.error = error?.error?.message || 'An error occurred!'; }, }); } copyUrl(url: string) { navigator.clipboard .writeText(url) .then(() => { // Optional: Display a message or perform an action after successful copy console.log('URL copied to clipboard!'); this.copyMessage = 'Copied!'; setTimeout(() => { this.copyMessage = ''; }, 2000); }) .catch((err) => { console.error('Failed to copy URL: ', err); this.copyMessage = 'Failed to copy URL'; }); } copyListUrl(url: string, index: number) { navigator.clipboard .writeText(url) .then(() => { // Optional: Display a message or perform an action after successful copy console.log('URL copied to clipboard!'); this.copyListMessage = 'Copied!'; this.copyIndex = index; setTimeout(() => { this.copyListMessage = ''; this.copyIndex = -1; }, 2000); }) .catch((err) => { console.error('Failed to copy URL: ', err); this.copyListMessage = 'Failed to copy URL'; }); } prepareDelete(url: string) { this.urlToDelete = url; this.showDeleteModal = true; } confirmDelete() { // Close the modal this.showDeleteModal = false; // Delete the URL this.deleteUrl(this.urlToDelete); } deleteUrl(id: string) { this.urlService.deleteUrl(id).subscribe({ next: (response) => { // console.log('Deleted URL: ', response); this.getAllUrls(); }, error: (error) => { console.error('Error deleting URL: ', error); this.error = error?.error?.message || 'An error occurred!'; }, }); } ngOnDestroy() { this.unsubscribe$.next(); this.unsubscribe$.complete(); } }
<router-outlet></router-outlet>
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(), ], };
import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', loadComponent: () => import('./shorten/shorten.component').then((m) => m.ShortenComponent), }, ];
Anda telah berjaya membina aplikasi pemendek URL menggunakan Angular dan Tailwind CSS. Projek ini menunjukkan cara mengintegrasikan teknologi bahagian hadapan moden untuk mencipta aplikasi web yang berfungsi dan bergaya. Dengan ciri berkuasa Angular dan pendekatan mengutamakan utiliti Tailwind CSS, anda boleh membina aplikasi web yang responsif dan cekap dengan mudah.
Jangan ragu untuk melanjutkan aplikasi ini dengan menambahkan ciri seperti pengesahan pengguna, dll. Selamat mengekod!
Lawati repositori GitHub untuk meneroka kod secara terperinci.
Atas ialah kandungan terperinci Membina Apl Pemendek URL dengan CSS Sudut dan Tailwind. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!