From 3fd0ba75f0802e10872564ee3c7e824403e0f294 Mon Sep 17 00:00:00 2001 From: Ian Andwati Date: Wed, 12 Mar 2025 22:51:07 +0300 Subject: [PATCH] mvp --- main.go | 178 +++++++++++++++++++++++++++++++++++++++++- static/script.js | 0 templates/index.html | 2 +- templates/index1.html | 55 ------------- templates/stats.html | 2 +- 5 files changed, 179 insertions(+), 58 deletions(-) create mode 100644 static/script.js delete mode 100644 templates/index1.html diff --git a/main.go b/main.go index 269674f..7c754fa 100644 --- a/main.go +++ b/main.go @@ -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(` +
+

Your shortened URL:

+ %s + +
+ `, 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 := ` + + + + + + + + + ` + + for _, url := range urls { + shortURL := fmt.Sprintf("%s/%s", baseURL, url.ShortCode) + html += fmt.Sprintf(` + + + + + + + `, url.OriginalURL, truncateString(url.OriginalURL, 30), shortURL, shortURL, url.CreatedAt.Format("2006-01-02"), url.Visits) + } + + html += `
Original URL Short URL Created Visists
%s%s%s%d
` + 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()) +} diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..e69de29 diff --git a/templates/index.html b/templates/index.html index 84ef022..89390c6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -42,7 +42,7 @@ -
+
diff --git a/templates/index1.html b/templates/index1.html deleted file mode 100644 index be86b73..0000000 --- a/templates/index1.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - URL shortener - - - -
- - -
- -
-
- - -
-
- -
- - -
-
-
-
- footer -
- - - \ No newline at end of file diff --git a/templates/stats.html b/templates/stats.html index 80783a9..df29441 100644 --- a/templates/stats.html +++ b/templates/stats.html @@ -36,7 +36,7 @@

Top 10 URLS

-
+
Loading stats...