main page, routing, db connection

This commit is contained in:
2025-03-12 17:30:43 +03:00
parent 74af30ef57
commit 375263d69c
6 changed files with 386 additions and 42 deletions
+6
View File
@@ -0,0 +1,6 @@
DB_HOST=localhost
DB_USER=postgres
DB_NAME=urlshortener
DB_PORT=5432
DB_PASSWORD=postgres
PORT=8080
+90
View File
@@ -0,0 +1,90 @@
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
)
type URLShortener struct {
DB *sql.DB
}
type URL struct {
ID int
OriginalURL string
ShortCode string
CreatedAt string
Visits int
}
func main() {
dbHost := getEnv("DB_HOST", "localhost")
dbPort := getEnv("DB_PORT", "5432")
dbUser := getEnv("DB_USER", "postgres")
dbPassword := getEnv("DB_PASSWORD", "postgres")
dbName := getEnv("DB_NAME", "urlshortener")
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
dbHost, dbPort, dbUser, dbPassword, dbName)
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatalf("Failed to connect to the database: %v", err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatalf("Failed to ping the database: %v", err)
}
err = initializeDB(db)
if err != nil {
log.Fatalf("Failed to initialize the DB: %v", err)
}
shortener := &URLShortener{DB: db}
router := mux.NewRouter()
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
router.HandleFunc("/", shortener.homeHandler).Methods("GET")
port := getEnv("PORT", "8080")
log.Printf("Server starting on port http://localhost:%s", port)
if err := http.ListenAndServe(":"+port, router); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
func getEnv(key, fallback string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return fallback
}
func initializeDB(db *sql.DB) error {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS urls(
id SERIAL PRIMARY KEY,
original_url TEXT NOT NULL,
short_code VARCHAR(10) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
visits INTEGER DEFAULT 0
)
`)
return err
}
func (s *URLShortener) homeHandler(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./templates/index.html")
}
+148
View File
@@ -0,0 +1,148 @@
:root{
--primary-color: #1095c1;
--secondary-color: #98c1d9;
--accent-color: #3d5a80;
--background-color: #f8f9fa;
--text-color:#293242;
}
body{
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--background-color);
color: var(--text-color);
}
main{
flex: 1;
padding: 2rem 0;
}
h1{
text-align: center;
margin-bottom: 2rem;
color: var(--accent-color);
}
nav{
margin-bottom: 2rem;
}
nav ul{
display: flex;
justify-content: center;
padding: 0;
list-style: none;
}
nav ul li{
margin: 0 1rem;
}
nav ul li a {
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
nav ul li a.active{
background-color: var(--primary-color);
color: white;
}
form{
margin-bottom: 2rem;
}
.shorten-btn{
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.shorten-btn:hover{
background-color: var(--accent-color);
border-color: var(--accent-color);
}
.result-container{
background-color: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-top: 2rem;
text-align: center;
}
.result-container a{
color: var(--primary-color);
word-break: break-all;
display: block;
margin: 1rem 0;
font-size: 1.2rem;
}
.copy-btn{
background-color: var(--secondary-color);
border-color: var(--secondary-color);
color: var(--text-color)
}
.copy-btn:hover{
background-color: var(--accent-color);
border-color: var(--accent-color);
color: white;
}
.stats-container{
background-color: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
table{
width: 100%;
border-collapse: collapse;
}
tab th, table td{
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
table th{
background-color: var(--secondary-color);
color: var(--text-color);
}
table tr:hover{
background-color: rgba(152, 193, 217, 0.1);
}
.loading{
text-align: center;
padding: 2rem;
color: #6c757d;
}
footer{
text-align: center;
padding: 1rem 0;
margin-top: 2rem;
background-color: white;
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.05);
}
@media (max-width: 768px) {
.grid{
grid-template-columns: 1fr;
}
nav ul li {
margin: 0 0.5rem;
}
}
+30 -18
View File
@@ -1,19 +1,22 @@
<!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>
<!-- <link rel="stylesheet" href="/static/style.css"> -->
<link rel="stylesheet" href="/static/pico.min.css">
<script src="/static/htmx.min.js"></script>
</head>
<body>
<main class="container-fluid">
<main class="container">
<h1>URL shortener</h1>
<nav>
<ul><li><strong>Link shortener</strong></li></ul>
<ul>
<li><a href="/" class="active">Home</a></li>
<li><a href="/stats" hx-get="/stats" hx-push-url="true" hx-target="main">Stats</a></li>
</ul>
<ul>
<li>
@@ -32,24 +35,33 @@
</ul>
</nav>
<section class="container">
<form action="">
<section>
<form hx-post="/shorten" hx-target="#result-container" hx-swap="innerHTML" >
<fieldset role="group">
<input type="text" name="" id="">
<input type="url" name="url" id="url" placeholder="https://example.com/long/url" required>
<button type="submit">Shorten</button>
</fieldset>
</form>
<div class="result-container">
<div>
<input type="text">
<button>copy</button>
</div>
</section>
</main>
<footer class="container-fluid">
<small>footer</small>
</footer>
</body>
</main>
<footer class="container">
<small>Powered by <a href="https://github.com/andwati/url-shortener" target="_blank">URL Shortener</a></small>
</footer>
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function(){
alert('URL copied to clipboard!');
}, function(err){
console.error('Could not copy text: ', err)
}
)
}
</script>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
<!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>
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL shortener - stats</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/pico.min.css">
<script src="/static/htmx.min.js"></script>
</head>
<body>
<main class="container">
<h1>URL shortener statistics</h1>
<nav>
<ul>
<li><a href="/" hx-get="/" hx-push-url="true" hx-target="main">Home</a></li>
<li><a href="/stats" class="active" >Stats</a></li>
</ul>
</nav>
<section>
<h2>Top 10 URLS</h2>
<div class="stats-container" hx-get="/stats" hx-trigger="load once">
<div class="loading">Loading stats...</div>
</div>
</section>
</main>
<footer class="container">
<small>Powered by <a href="https://github.com/andwati/url-shortener" target="_blank">URL Shortener</a></small>
</footer>
</body>
</html>