mirror of
https://github.com/andwati/url-shortener.git
synced 2026-10-18 02:32:53 +00:00
mvp
This commit is contained in:
@@ -4,8 +4,10 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
_ "github.com/lib/pq"
|
||||
@@ -19,7 +21,7 @@ type URL struct {
|
||||
ID int
|
||||
OriginalURL string
|
||||
ShortCode string
|
||||
CreatedAt string
|
||||
CreatedAt time.Time
|
||||
Visits int
|
||||
}
|
||||
|
||||
@@ -57,6 +59,9 @@ func main() {
|
||||
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
|
||||
|
||||
router.HandleFunc("/", shortener.homeHandler).Methods("GET")
|
||||
router.HandleFunc("/shorten", shortener.shortenHandler).Methods("POST")
|
||||
router.HandleFunc("/stats", shortener.statsHandler).Methods("GET")
|
||||
router.HandleFunc("/{shortCode}", shortener.redirectHandler).Methods("GET")
|
||||
|
||||
port := getEnv("PORT", "8080")
|
||||
log.Printf("Server starting on port http://localhost:%s", port)
|
||||
@@ -88,3 +93,174 @@ func initializeDB(db *sql.DB) error {
|
||||
func (s *URLShortener) homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./templates/index.html")
|
||||
}
|
||||
|
||||
func (s *URLShortener) shortenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, "Error parsing form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
originalURL := r.FormValue("url")
|
||||
if originalURL == "" {
|
||||
http.Error(w, "URL is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
shortcode := generateShortCode(6)
|
||||
|
||||
_, err = s.DB.Exec(
|
||||
"INSERT INTO urls (original_url, short_code) VALUES ($1, $2)",
|
||||
originalURL, shortcode,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error inserting URL: %v", err)
|
||||
http.Error(w, "Error creating short url", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
baseURL := fmt.Sprintf("%s://%s", getScheme(r), r.Host)
|
||||
shortURL := fmt.Sprintf("%s/%s", baseURL, shortcode)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(fmt.Sprintf(`
|
||||
<div id="result" class="result-container">
|
||||
<p>Your shortened URL:</p>
|
||||
<a href="%s" target="_blank">%s</a>
|
||||
<button class="copy-btn" onclick="copyToClipBoard('%s')">Copy</button>
|
||||
</div>
|
||||
`, shortURL, shortURL, shortURL)))
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *URLShortener) redirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
shortCode := vars["shortCode"]
|
||||
|
||||
var url URL
|
||||
err := s.DB.QueryRow(
|
||||
"SELECT id, original_url FROM urls WHERE short_code = $1",
|
||||
shortCode,
|
||||
).Scan(&url.ID, &url.OriginalURL)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
log.Printf("Database error: %v", err)
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.DB.Exec("UPDATE urls SET visits = visits +1 WHERE id = $1", url.ID)
|
||||
if err != nil {
|
||||
log.Printf("Error updating visit count: %v", err)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, url.OriginalURL, http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func (s *URLShortener) statsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := s.DB.Query(`
|
||||
SELECT original_url, short_code, created_at, visits
|
||||
FROM urls
|
||||
ORDER BY visits DESC
|
||||
LIMIT 10
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error querying stats: %v", err)
|
||||
http.Error(w, "Error fetching stats", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var urls []URL
|
||||
baseURL := fmt.Sprintf("%s://%s", getScheme(r), r.Host)
|
||||
|
||||
for rows.Next() {
|
||||
var url URL
|
||||
var createdAt time.Time
|
||||
err := rows.Scan(&url.OriginalURL, &url.ShortCode, &createdAt, &url.Visits)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Errow scanning row: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
url.CreatedAt = createdAt
|
||||
urls = append(urls, url)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
log.Printf("Error iterating rows: %v", err)
|
||||
http.Error(w, "Error processing stats", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
html := `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Original URL </th>
|
||||
<th> Short URL </th>
|
||||
<th> Created </th>
|
||||
<th> Visists </th>
|
||||
</tr>
|
||||
</thead>
|
||||
</tbody>`
|
||||
|
||||
for _, url := range urls {
|
||||
shortURL := fmt.Sprintf("%s/%s", baseURL, url.ShortCode)
|
||||
html += fmt.Sprintf(`
|
||||
<tr>
|
||||
<td><a href="%s" target="_blank">%s</a></td>
|
||||
<td><a href="%s" target="_blank">%s</a></td>
|
||||
<td>%s</td>
|
||||
<td>%d</td>
|
||||
</tr>
|
||||
`, url.OriginalURL, truncateString(url.OriginalURL, 30), shortURL, shortURL, url.CreatedAt.Format("2006-01-02"), url.Visits)
|
||||
}
|
||||
|
||||
html += `</tbody></table>`
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, "./templates/stats.html")
|
||||
}
|
||||
|
||||
func generateShortCode(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
result[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func getScheme(r *http.Request) string {
|
||||
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLength int) string {
|
||||
if len(s) <= maxLength {
|
||||
return s
|
||||
}
|
||||
|
||||
return s[:maxLength-3] + "..."
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<button type="submit">Shorten</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div class="result-container">
|
||||
<div class="result-container" id="result-container">
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<link rel="stylesheet" href="pico.min.css">
|
||||
<script src="htmx.min.js"></script>
|
||||
<title>URL shortener</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="container-fluid">
|
||||
<nav>
|
||||
<ul><li><strong>Link shortener</strong></li></ul>
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<details class="dropdown">
|
||||
<summary>
|
||||
Account
|
||||
</summary>
|
||||
<ul dir="rtl">
|
||||
<li><a href="">Profile</a></li>
|
||||
<li><a href="">Settings</a></li>
|
||||
<li><a href="">Security</a></li>
|
||||
<li><a href="">Logout</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<section class="container">
|
||||
|
||||
<form action="">
|
||||
<fieldset role="group">
|
||||
<input type="text" name="" id="">
|
||||
<button type="submit">Shorten</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<input type="text">
|
||||
<button>copy</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="container-fluid">
|
||||
<small>footer</small>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<section>
|
||||
<h2>Top 10 URLS</h2>
|
||||
<div class="stats-container" hx-get="/stats" hx-trigger="load once">
|
||||
<div class="stats-container" hx-get="/stats" >
|
||||
<div class="loading">Loading stats...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user