Membina Apl Pemendek URL dengan CSS Sudut dan Tailwind

WBOY
Lepaskan: 2024-07-26 18:53:31
asal
1094 orang telah melayarinya

Building a URL Shortener App with Angular and Tailwind CSS

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.

Prasyarat

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.

Persediaan Projek

1. Mencipta Projek Sudut Baharu

Mula-mula, buat projek Angular baharu dengan menjalankan arahan berikut dalam terminal anda:

ng new url-shortener-app
cd url-shortener-app
Salin selepas log masuk

2. Memasang Tailwind CSS

Seterusnya, sediakan Tailwind CSS dalam projek Angular anda. Pasang Tailwind CSS dan kebergantungannya melalui npm:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
Salin selepas log masuk

Konfigurasikan CSS Tailwind dengan mengemas kini fail tailwind.config.js:

module.exports = {
  content: [
    "./src/**/*.{html,ts}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
Salin selepas log masuk

Tambahkan arahan Tailwind pada fail src/styles.scss anda:

@tailwind base;
@tailwind components;
@tailwind utilities;
Salin selepas log masuk

Membina Pemendek URL

3. Mencipta Model URL

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;
}
Salin selepas log masuk

4. Menyediakan Perkhidmatan URL

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}`);
  }
}
Salin selepas log masuk

5. Mencipta Komponen URL Pendek

Jana komponen baharu untuk memendekkan URL:

ng generate component shorten
Salin selepas log masuk

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>
}
Salin selepas log masuk

6. Menambah Logik pada Komponen

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

Salin selepas log masuk

7. Kemas kini fail HTML komponen apl (src/app/app.component.html)

<router-outlet></router-outlet>
Salin selepas log masuk

8. Kemas kini fail konfigurasi aplikasi (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(),
  ],
};
Salin selepas log masuk

9. Kemas kini fail laluan apl (src/app/app.routes.ts)

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./shorten/shorten.component').then((m) => m.ShortenComponent),
  },
];
Salin selepas log masuk

Kesimpulan

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!

Meneroka Kod

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!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan