このブログでは、フロントエンドに Angular、スタイル設定に Tailwind CSS を使用して URL 短縮アプリケーションを作成するプロセスを説明します。 URL 短縮ツールは、長い URL をより短く、管理しやすいリンクに変換する便利なツールです。このプロジェクトは、最新の Web 開発テクノロジを使用して、機能的で見た目にも美しい Web アプリケーションを構築する方法を理解するのに役立ちます。
このチュートリアルを進めるには、Angular の基本を理解し、Tailwind CSS についてある程度の知識が必要です。 Node.js と Angular CLI がマシンにインストールされていることを確認してください。
まず、ターミナルで次のコマンドを実行して、新しい Angular プロジェクトを作成します。
ng new url-shortener-app cd url-shortener-app
次に、Angular プロジェクトで Tailwind CSS をセットアップします。 Tailwind CSS とその依存関係を npm 経由でインストールします:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init
tailwind.config.js ファイルを更新して、Tailwind CSS を構成します:
module.exports = { content: [ "./src/**/*.{html,ts}", ], theme: { extend: {}, }, plugins: [], }
Tailwind ディレクティブを src/styles.scss ファイルに追加します。
@tailwind base; @tailwind components; @tailwind utilities;
URL モデルを作成して URL データの構造を定義します。新しいファイル 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; }
URL 短縮に関連する API 呼び出しを処理するサービスを作成します。新しいファイル 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}`); } }
URL を短縮するための新しいコンポーネントを生成します:
ng generate component shorten
コンポーネントの HTML (src/app/shorten/shorten.component.html) を以下のように更新します。
<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> }
フォーム送信と API インタラクションを処理するために、コンポーネントの TypeScript ファイル (src/app/shorten/shorten.component.ts) を更新します。
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), }, ];
Angular と Tailwind CSS を使用して URL 短縮アプリケーションを正常に構築しました。このプロジェクトでは、最新のフロントエンド テクノロジを統合して、機能的でスタイリッシュな Web アプリケーションを作成する方法を示します。 Angular の強力な機能と Tailwind CSS のユーティリティ第一のアプローチにより、応答性が高く効率的な Web アプリケーションを簡単に構築できます。
ユーザー認証などの機能を追加して、このアプリケーションを自由に拡張してください。コーディングを楽しんでください!
コードを詳しく調べるには、GitHub リポジトリにアクセスしてください。
以上がAngular および Tailwind CSS を使用した URL 短縮アプリの構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。