首頁 >web前端 >js教程 >使用 Angular 和 Tailwind CSS 建立 URL 縮短應用程式

使用 Angular 和 Tailwind CSS 建立 URL 縮短應用程式

WBOY
WBOY原創
2024-07-26 18:53:311127瀏覽

Building a URL Shortener App with Angular and Tailwind CSS

在本部落格中,我們將引導您完成使用 Angular 作為前端並使用 Tailwind CSS 進行樣式建立 URL 縮短應用程式的過程。 URL 縮短器是一個方便的工具,可以將長 URL 轉換為更短、更易於管理的連結。此專案將幫助您了解如何使用現代 Web 開發技術建立功能齊全且美觀的 Web 應用程式。

先決條件

要學習本教程,您應該對 Angular 有基本的了解,並對 Tailwind CSS 有一定的了解。確保您的電腦上安裝了 Node.js 和 Angular CLI。

項目設定

1. 建立一個新的 Angular 項目

首先,透過在終端機中執行以下命令來建立一個新的 Angular 專案:

ng new url-shortener-app
cd url-shortener-app

2. 安裝 Tailwind CSS

接下來,在您的 Angular 專案中設定 Tailwind CSS。透過 npm 安裝 Tailwind CSS 及其依賴項:

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 縮短器

3. 建立 URL 模型

建立 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;
}

4. 設定 URL 服務

建立一個服務來處理與 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}`);
  }
}

5. 建立縮短 URL 元件

產生一個用於縮短 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>
}

6. 在元件中加入邏輯

更新元件的 TypeScript 檔案 (src/app/shorten/shorten.component.ts) 以處理表單提交和 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();
  }
}

7.更新應用程式元件的HTML檔(src/app/app.component.html)

<router-outlet></router-outlet>

8.更新應用程式設定檔(src/app/app.config.ts)

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

9.更新應用程式路由檔案(src/app/app.routes.ts)

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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn