inicjalizacja projektu dodanie nowych stylow itp

This commit is contained in:
mateusz779 2024-11-06 23:38:59 +01:00
commit e587c7a59c
9 changed files with 1043 additions and 0 deletions

27
Dockerfile Normal file
View File

@ -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"]

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module mkedziora/fast-links
go 1.23.2

175
main.go Normal file
View File

@ -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)
}

87
public/app.html Normal file
View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<!-- Author:Mateusz Kędziora https://mkedziora.pl -->
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>App</title>
<meta name="robots" content="noindex" />
<link href="/style_app.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<h1 id="info">Click the button to start.</h1>
<a href="" id="url" rel="noopener noreferrer" target="_blank"></a>
<button id="startBtn">Start listening</button>
</div>
<script>
let lastData = "";
// Pobieranie parametru id z URL
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get("id");
let new_url = "";
let fetchingData;
let newWindow;
document
.getElementById("startBtn")
.addEventListener("click", function () {
document.getElementById("info").textContent =
"Waiting for a new address...";
newWindow = window.open("about:blank", "_blank", "");
fetchingData = setInterval(checkForNewAddress, 1000);
});
function checkForNewAddress() {
// Dodanie parametru id do URL zapytania
const apiUrl = id
? `/api/get?id=${encodeURIComponent(id)}`
: "/api/get";
fetch(apiUrl, {
method: "GET",
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache",
},
})
.then((response) => {
if (!response.ok) {
throw new Error("Brak danych");
}
return response.json();
})
.then((data) => {
if (data.data && data.data !== lastData) {
lastData = data.data;
console.log("Nowy adres:", data.data);
// Przejście do nowego adresu z resetowaniem nagłówków
const url = new URL(data.data);
const a = document.getElementById("url");
a.href = url;
a.textContent = url;
console.log(newWindow);
if (newWindow) {
console.log("ustawiono nowy adres");
newWindow.location.href = id
? `/api/url?id=${encodeURIComponent(id)}`
: "/api/url";
}
document.getElementById("info").textContent =
"The page has opened; if not, click the link below. Link waiting is disabled.";
clearInterval(fetchingData);
}
})
.catch((error) => {
console.error("Błąd:", error);
});
}
// Sprawdzaj co sekundę
</script>
</body>
</html>

301
public/index.html Normal file
View File

@ -0,0 +1,301 @@
<!DOCTYPE html>
<!-- Author:Mateusz Kędziora https://mkedziora.pl -->
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5-qrcode/2.3.8/html5-qrcode.min.js"></script>
<link href="/style_index.css" rel="stylesheet" />
</head>
<body>
<div class="controls">
<button id="startButton">Uruchom skaner QR</button>
<button id="stopButton" style="display: none">Zatrzymaj skaner</button>
</div>
<div class="manual-input">
<label for="idInput">ID (opcjonalne):</label>
<input type="text" id="idInput" placeholder="Wprowadź ID" />
<label for="expire">Czas wygaśnięcia (opcjonalne):</label>
<input
type="number"
min="1"
value="30"
max="86400"
id="expireInput"
placeholder="Czas wygaśnięcia"
/>
</div>
<div class="zoom-control" style="display: none">
<label for="zoomSlider">Powiększenie kamery:</label>
<input
type="range"
id="zoomSlider"
min="1"
max="10"
step="0.1"
value="1"
/>
<div class="zoom-value">1x</div>
</div>
<div id="reader"></div>
<div class="or-divider">
<span class="or-text">LUB</span>
</div>
<div class="manual-input">
<label for="manualUrl">Wprowadź URL ręcznie:</label>
<input type="url" id="manualUrl" placeholder="https://przykład.com" />
<button id="submitUrl">Wyślij URL</button>
</div>
<div id="status"></div>
<script>
let currentStream = null;
let html5QrcodeScanner = null;
const statusDiv = document.getElementById("status");
const startButton = document.getElementById("startButton");
const stopButton = document.getElementById("stopButton");
const submitUrlButton = document.getElementById("submitUrl");
const manualUrlInput = document.getElementById("manualUrl");
// Funkcja do obsługi i wyświetlania błędów
function handleError(error) {
console.error("Wystąpił błąd:", error);
let errorMessage = "Wystąpił nieoczekiwany błąd.";
if (error instanceof Error) {
switch (error.name) {
case "NotAllowedError":
errorMessage =
"Dostęp do kamery został zabroniony. Sprawdź ustawienia przeglądarki.";
break;
case "NotFoundError":
errorMessage =
"Nie znaleziono kamery. Sprawdź czy urządzenie ma kamerę.";
break;
case "NotReadableError":
errorMessage =
"Kamera jest obecnie używana przez inną aplikację.";
break;
case "OverconstrainedError":
errorMessage =
"Nie znaleziono odpowiedniej kamery. Spróbuj ponownie.";
break;
default:
errorMessage =
error.message ||
"Wystąpił problem z kamerą. Spróbuj odświeżyć stronę.";
}
}
statusDiv.textContent = errorMessage;
return errorMessage;
}
async function sendUrl(url) {
try {
statusDiv.textContent = "Wysyłanie danych...";
const id = document.getElementById("idInput").value.trim();
const expire = document.getElementById("expireInput").value.trim();
const body = { value: url };
if (id) body.id = id;
if (expire) body.expire = expire;
const response = await fetch("/api/set", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (response.ok) {
statusDiv.textContent = "Sukces! URL został przesłany.";
return true;
} else {
throw new Error("Błąd podczas wysyłania danych na serwer");
}
} catch (error) {
handleError(error);
return false;
}
}
async function stopScanner() {
try {
if (html5QrcodeScanner) {
await html5QrcodeScanner.stop();
html5QrcodeScanner.clear();
html5QrcodeScanner = null;
}
if (currentStream) {
const tracks = currentStream.getTracks();
tracks.forEach((track) => {
if (track.readyState === "live") {
track.stop();
}
});
currentStream = null;
}
document.querySelector(".zoom-control").style.display = "none";
startButton.style.display = "block";
stopButton.style.display = "none";
const readerElement = document.getElementById("reader");
if (readerElement) {
readerElement.innerHTML = "";
}
} catch (error) {
handleError(error);
}
}
startButton.addEventListener("click", async function () {
const zoomControl = document.querySelector(".zoom-control");
const zoomSlider = document.getElementById("zoomSlider");
const zoomValue = document.querySelector(".zoom-value");
try {
// Najpierw zatrzymaj poprzedni skaner
await stopScanner();
// Poczekaj chwilę przed ponownym uruchomieniem
await new Promise((resolve) => setTimeout(resolve, 500));
// Sprawdź dostępne urządzenia
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(
(device) => device.kind === "videoinput"
);
if (videoDevices.length === 0) {
throw new Error("Nie znaleziono żadnej kamery w urządzeniu");
}
// Preferuj tylną kamerę
const camera =
videoDevices.find(
(device) =>
device.label.toLowerCase().includes("back") ||
device.label.toLowerCase().includes("tylna")
) || videoDevices[videoDevices.length - 1];
const stream = await navigator.mediaDevices.getUserMedia({
video: {
deviceId: camera.deviceId,
facingMode: { ideal: "environment" },
},
});
currentStream = stream;
const track = stream.getVideoTracks()[0];
if (!track) {
throw new Error("Nie udało się uzyskać dostępu do kamery");
}
// Sprawdź możliwości kamery
const capabilities = track.getCapabilities();
if (capabilities && capabilities.zoom) {
zoomControl.style.display = "block";
zoomSlider.min = capabilities.zoom.min;
zoomSlider.max = capabilities.zoom.max;
zoomSlider.step =
(capabilities.zoom.max - capabilities.zoom.min) / 100;
zoomSlider.value = capabilities.zoom.min;
zoomValue.textContent = `${capabilities.zoom.min}x`;
zoomSlider.addEventListener("input", async (e) => {
try {
const zoomValue = parseFloat(e.target.value);
await track.applyConstraints({
advanced: [{ zoom: zoomValue }],
});
document.querySelector(
".zoom-value"
).textContent = `${zoomValue.toFixed(1)}x`;
} catch (error) {
console.warn("Nie udało się ustawić zoomu:", error);
}
});
} else {
zoomControl.style.display = "none";
}
html5QrcodeScanner = new Html5Qrcode("reader");
const qrConfig = {
fps: 10,
qrbox: { width: 250, height: 250 },
aspectRatio: 1.0,
};
startButton.style.display = "none";
stopButton.style.display = "block";
statusDiv.textContent = "Skaner jest aktywny...";
await html5QrcodeScanner.start(
{ facingMode: "environment" },
qrConfig,
async (decodedText) => {
await stopScanner();
if (await sendUrl(decodedText)) {
manualUrlInput.value = decodedText;
}
},
(error) => {
// Ignorujemy błędy skanowania, bo pojawiają się często gdy nie ma kodu QR w kadrze
console.debug("Skanowanie:", error);
}
);
} catch (error) {
await stopScanner();
handleError(error);
}
});
stopButton.addEventListener("click", stopScanner);
window.addEventListener("beforeunload", stopScanner);
document.addEventListener("visibilitychange", () => {
if (document.hidden && html5QrcodeScanner) {
stopScanner();
}
});
submitUrlButton.addEventListener("click", async () => {
const url = manualUrlInput.value.trim();
if (!url) {
statusDiv.textContent = "Proszę wprowadzić URL";
return;
}
try {
new URL(url); // Sprawdź poprawność URL
await sendUrl(url);
} catch (error) {
if (error instanceof TypeError) {
statusDiv.textContent = "Wprowadzony URL jest nieprawidłowy";
} else {
handleError(error);
}
}
});
manualUrlInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
submitUrlButton.click();
}
});
</script>
</body>
</html>

121
public/style_app.css Normal file
View File

@ -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;
}
}

241
public/style_index.css Normal file
View File

@ -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;
}
}

38
readme.md Normal file
View File

@ -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.

50
run_container.sh Executable file
View File

@ -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"