commit e587c7a59cf547f4f2cdde0e0d5b623257a6b14c Author: mateusz779 Date: Wed Nov 6 23:38:59 2024 +0100 inicjalizacja projektu dodanie nowych stylow itp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d96f05d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Author:Mateusz Kędziora https://mkedziora.pl + +FROM golang:1.23.2-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum* ./ + +RUN go mod download + +COPY *.go ./ + +RUN CGO_ENABLED=0 GOOS=linux go build -o main . + +FROM alpine:latest + +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +COPY --from=builder /app/main . + +COPY public/ ./public + +EXPOSE 8000 + +CMD ["./main"] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d19f5dd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module mkedziora/fast-links + +go 1.23.2 diff --git a/main.go b/main.go new file mode 100644 index 0000000..97ba9c7 --- /dev/null +++ b/main.go @@ -0,0 +1,175 @@ +package main + +// Author:Mateusz Kędziora https://mkedziora.pl + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "sync" + "time" +) + +type DataEntry struct { + Value string + Timestamp time.Time +} + +var ( + dataMap map[string]DataEntry + mu sync.Mutex +) + +func init() { + dataMap = make(map[string]DataEntry) +} + +func setHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + return + } + + var input struct { + Data string `json:"value"` + ID string `json:"id,omitempty"` + Expire string `json:"expire,omitempty"` + } + + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + + mu.Lock() + if input.ID == "" { + input.ID = "default" + } + + expireString, err := strconv.Atoi(input.Expire) + if err != nil { + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + + expireSeconds := 30 + if expireString > 0 { + if expireString > 86400 { + http.Error(w, fmt.Sprintf("Maksymalny czas wygaśnięcia to %d sekund", 86400), http.StatusBadRequest) + return + } + expireSeconds = expireString + } + + expirationTime := time.Now().Add(time.Duration(expireSeconds) * time.Second) + + dataMap[input.ID] = DataEntry{ + Value: input.Data, + Timestamp: expirationTime, + } + mu.Unlock() + + w.WriteHeader(http.StatusNoContent) +} + +func getHandler(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + id = "default" + } + + mu.Lock() + entry, exists := dataMap[id] + mu.Unlock() + + if !exists || time.Since(entry.Timestamp) > 30*time.Second { + delete(dataMap, id) + http.Error(w, "No data available", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"data": entry.Value}) +} + +func getUrlHandler(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + id = "default" + } + + mu.Lock() + entry, exists := dataMap[id] + mu.Unlock() + + if !exists || time.Since(entry.Timestamp) > 30*time.Second { + delete(dataMap, id) + http.Error(w, "No data available", http.StatusNotFound) + return + } + + w.Header().Set("Referrer-Policy", "no-referrer") + http.Redirect(w, r, entry.Value, http.StatusFound) +} + +func testHandler(w http.ResponseWriter, r *http.Request) { + // Utworzenie mapy do przechowywania nagłówków + headers := make(map[string]string) + + // Iteracja przez wszystkie nagłówki + for name, values := range r.Header { + // Używamy wartości 0, ponieważ nagłówki mogą mieć wiele wartości + headers[name] = values[0] + } + + // Ustawienie nagłówka Content-Type na application/json + w.Header().Set("Content-Type", "application/json") + + // Zwrócenie nagłówków w formacie JSON + json.NewEncoder(w).Encode(headers) +} + +func main() { + // Obsługa endpointów API + http.HandleFunc("/api/set", setHandler) + http.HandleFunc("/api/get", getHandler) + http.HandleFunc("/api/url", getUrlHandler) + http.HandleFunc("/test", testHandler) + + // Serwowanie plików statycznych + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + http.ServeFile(w, r, "public/index.html") + } else { + http.NotFound(w, r) + } + }) + + http.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "public/app.html") + }) + + http.HandleFunc("/style_index.css", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "public/style_index.css") + }) + + http.HandleFunc("/style_app.css", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "public/style_app.css") + }) + + go func() { + for { + for id, timestamp := range dataMap { + if time.Now().After(timestamp.Timestamp) { + delete(dataMap, id) + break + } + } + time.Sleep(5 * time.Second) + } + }() + + // Uruchomienie serwera + http.ListenAndServe(":8080", nil) +} diff --git a/public/app.html b/public/app.html new file mode 100644 index 0000000..1a000a8 --- /dev/null +++ b/public/app.html @@ -0,0 +1,87 @@ + + + + + + + App + + + + +
+

Click the button to start.

+ + +
+ + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..471452c --- /dev/null +++ b/public/index.html @@ -0,0 +1,301 @@ + + + + + + + + + + + +
+ + +
+ +
+ + + + + +
+ + + +
+ +
+ LUB +
+ +
+ + + +
+ +
+ + + + diff --git a/public/style_app.css b/public/style_app.css new file mode 100644 index 0000000..10e4658 --- /dev/null +++ b/public/style_app.css @@ -0,0 +1,121 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #3498db; + --primary-hover: #2980b9; + --text-color: #2c3e50; + --background-color: #f5f7fa; + --shadow-color: rgba(0, 0, 0, 0.1); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, var(--background-color) 0%, #c3cfe2 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + color: var(--text-color); +} + +.container { + max-width: 600px; + width: 100%; + background-color: white; + border-radius: 15px; + box-shadow: 0 4px 15px var(--shadow-color); + padding: 2rem; + text-align: center; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +h1 { + font-size: 1.5rem; + color: var(--text-color); +} + +#startBtn { + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 25px; + padding: 0.8rem 1.5rem; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + -webkit-tap-highlight-color: transparent; + align-self: center; +} + +@media (hover: hover) { + #startBtn:hover { + background-color: var(--primary-hover); + transform: translateY(-2px); + box-shadow: 0 4px 15px var(--shadow-color); + } +} + +#startBtn:active { + transform: translateY(1px); +} + +#url { + color: var(--primary-color); + text-decoration: none; + font-size: 0.9rem; + word-break: break-all; +} + +#url:hover { + text-decoration: underline; +} + +/* Animacje */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.container { + animation: fadeIn 0.3s ease-in; +} + +/* Dostosowania dla małych ekranów */ +@media (max-width: 480px) { + .container { + padding: 1.5rem; + } + + h1 { + font-size: 1.3rem; + } + + #startBtn { + padding: 0.7rem 1.2rem; + font-size: 0.9rem; + } +} + +/* Ciemny motyw */ +@media (prefers-color-scheme: dark) { + :root { + --background-color: #1a1a1a; + --text-color: #ffffff; + --shadow-color: rgba(0, 0, 0, 0.3); + } + + body { + background: #1a1a1a; + } + + .container { + background-color: #2d2d2d; + } +} \ No newline at end of file diff --git a/public/style_index.css b/public/style_index.css new file mode 100644 index 0000000..90d6023 --- /dev/null +++ b/public/style_index.css @@ -0,0 +1,241 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #3498db; + --primary-hover: #2980b9; + --text-color: #2c3e50; + --background-color: #f5f7fa; + --border-color: #ddd; + --shadow-color: rgba(0, 0, 0, 0.1); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, var(--background-color) 0%, #c3cfe2 100%); + min-height: 100vh; + padding: 1rem; + color: var(--text-color); +} + +.container { + max-width: 600px; + margin: 0 auto; + background-color: white; + border-radius: 15px; + box-shadow: 0 4px 15px var(--shadow-color); + padding: 1.5rem; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + margin: 0; + } +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +button { + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 25px; + padding: 0.8rem 1.5rem; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.3s ease; + flex: 1; + min-width: 150px; + text-align: center; + -webkit-tap-highlight-color: transparent; +} + +@media (hover: hover) { + button:hover { + background-color: var(--primary-hover); + transform: translateY(-2px); + box-shadow: 0 4px 15px var(--shadow-color); + } +} + +button:active { + transform: translateY(1px); +} + +.manual-input { + margin-bottom: 1.5rem; +} + +label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--text-color); + font-size: 0.9rem; +} + +input[type="text"], +input[type="number"], +input[type="url"] { + width: 100%; + padding: 0.8rem; + border: 1px solid var(--border-color); + border-radius: 8px; + font-size: 1rem; + margin-bottom: 1rem; + -webkit-appearance: none; + appearance: none; +} + +input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); +} + +.zoom-control { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; + padding: 1rem; + background-color: #f8f9fa; + border-radius: 8px; +} + +#zoomSlider { + flex: 1; + -webkit-appearance: none; + appearance: none; + height: 4px; + background: var(--border-color); + border-radius: 2px; +} + +#zoomSlider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + background: var(--primary-color); + border-radius: 50%; + cursor: pointer; +} + +#reader { + width: 100%; + margin: 1.5rem 0; + overflow: hidden; + border-radius: 8px; +} + +.or-divider { + display: flex; + align-items: center; + margin: 2rem 0; + gap: 1rem; +} + +.or-divider::before, +.or-divider::after { + content: ''; + flex: 1; + height: 1px; + background-color: var(--border-color); +} + +.or-text { + color: #7f8c8d; + font-size: 0.9rem; + padding: 0 0.5rem; +} + +#status { + margin-top: 1rem; + padding: 1rem; + border-radius: 8px; + background-color: #f8f9fa; + font-size: 0.9rem; +} + +/* Dostosowania dla iOS */ +@supports (-webkit-touch-callout: none) { + input { + font-size: 16px; /* Zapobiega powiększaniu na iOS */ + } +} + +/* Ciemny motyw */ +@media (prefers-color-scheme: dark) { + :root { + --background-color: #1a1a1a; + --text-color: #ffffff; + --border-color: #333; + --shadow-color: rgba(0, 0, 0, 0.3); + } + + body { + background: #1a1a1a; + } + + .container { + background-color: #2d2d2d; + } + + input[type="text"], + input[type="number"], + input[type="url"] { + background-color: #333; + color: white; + } + + .zoom-control, + #status { + background-color: #333; + } +} + +/* Animacje */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.container { + animation: fadeIn 0.3s ease-in; +} + +/* Dostosowania dla małych ekranów */ +@media (max-width: 480px) { + body { + padding: 0.5rem; + } + + .container { + border-radius: 10px; + } + + button { + padding: 0.7rem 1.2rem; + } + + label { + font-size: 0.85rem; + } + + input[type="text"], + input[type="number"], + input[type="url"] { + padding: 0.7rem; + font-size: 0.9rem; + } +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c672190 --- /dev/null +++ b/readme.md @@ -0,0 +1,38 @@ +# Fast Links - Quickly Open Links on Multiple Devices + +## Project Goal + +Fast Links was created to streamline teamwork and increase productivity when working with multiple devices. The application allows for instant sharing of links in real-time by creating virtual rooms where all participants see the same links. + +## Use Cases + +- **Online Meetings**: Quickly share materials, presentations, and documents. +- **Teamwork**: Jointly browse websites, codes, and projects. +- **Education**: Use for interactive presentations and collaborative problem-solving. + +## Features + +- **Creating Virtual Rooms**: Each user can create their own room and invite others. +- **Adding Links**: Users add links that are immediately displayed on the screens of all room participants. +- **Scanning QR Codes**: Add links by scanning a QR code. +- **Scalability**: The application can handle any number of users and rooms. + +## How to Use + +1. **Installation**: Only Docker is required. +2. **Running**: Execute the `run_container.sh` script, optionally specifying the port using the `-p` parameter. +3. **Access**: Open the specified address in your browser. +4. **Adding Links**: Enter the link in the input field and confirm. +5. **Listening Mode**: Go to the `/app` page or specify a specific room ID using the `?id=` parameter. + +## Technologies + +- **Golang**: Efficient backend programming language. +- **html5-qrcode**: Library for scanning QR codes in the browser. + +## Additional Suggestions + +- **Documentation**: Consider creating a more detailed guide to explain advanced configuration and usage options. +- **User Interface**: Improving the UI could make the application more user-friendly. +- **Testing**: Conduct comprehensive tests to ensure application stability and reliability. +- **Security**: Ensure the security of the application, especially if it will be used to share sensitive data. \ No newline at end of file diff --git a/run_container.sh b/run_container.sh new file mode 100755 index 0000000..5f05750 --- /dev/null +++ b/run_container.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +# Author:Mateusz Kędziora https://mkedziora.pl + +# Nazwa projektu +PROJECT_NAME="fast-links" + +# Domyślny port +DEFAULT_PORT=8080 + +# Funkcja wyświetlająca sposób użycia +usage() { + echo "Użycie: $0 [-p port]" + echo " -p port Port na którym ma działać aplikacja (domyślnie: $DEFAULT_PORT)" + exit 1 +} + +# Przetwarzanie argumentów +while getopts ":p:" opt; do + case ${opt} in + p ) + PORT=$OPTARG + ;; + \? ) + usage + ;; + esac +done + +# Jeśli port nie został podany, użyj domyślnego +PORT=${PORT:-$DEFAULT_PORT} + +# Zatrzymaj i usuń stary kontener, jeśli istnieje +echo "Zatrzymywanie i usuwanie starego kontenera..." +docker stop $PROJECT_NAME 2>/dev/null +docker rm $PROJECT_NAME 2>/dev/null + +# Zbuduj nowy obraz +echo "Budowanie nowego obrazu..." +docker build -t $PROJECT_NAME . + +# Uruchom nowy kontener +echo "Uruchamianie nowego kontenera na porcie $PORT..." +docker run -d --name $PROJECT_NAME -p $PORT:8080 $PROJECT_NAME + +# Wyświetl informacje o uruchomionym kontenerze +echo "Kontener uruchomiony. Szczegóły:" +docker ps --filter name=$PROJECT_NAME + +echo "Aplikacja dostępna pod adresem: http://localhost:$PORT" \ No newline at end of file