This commit is contained in:
Marco Pedone 2025-08-04 17:45:44 +02:00
commit 29c64c7e39
481 changed files with 122434 additions and 0 deletions

35
.dockerignore Normal file
View file

@ -0,0 +1,35 @@
.gitignore
.git
readme.md
build_logs.txt
apps/backend/.git.bkp
apps/backend/images
apps/backend/storage
apps/backend/tmp
apps/backend/vscode
apps/backend/.air.toml
apps/backend/.git
apps/backend/.gitignore
apps/backend/bkp.xml
apps/backend/.bin
apps/backend/test.webp
apps/backend/TODO
apps/db/.git.bkp
apps/db/.git
apps/db/.gitignore
apps/infoalloggi/.git.bkp
apps/infoalloggi/.git
apps/infoalloggi/.gitignore
apps/infoalloggi/TODO
apps/infoalloggi/node_modules
apps/infoalloggi/.vscode
apps/infoalloggi/.next
apps/infoalloggi/Dockerfile
apps/infoalloggi/.env
apps/infoalloggi/.docker_env
apps/infoalloggi_old

37
.gitignore vendored Normal file
View file

@ -0,0 +1,37 @@
.env
build_logs.txt
infoalloggi_old/
apps/db/.git.bkp
apps/db/.env
apps/db/.gitignore
apps/backend/.git.bkp
apps/backend/.gitignore
apps/backend/tmp
apps/backend/images
apps/backend/bkp.xml
apps/backend/storage
apps/backend/.bin
apps/backend/caller.bat
apps/backend/*.exe
apps/backend/web.config
apps/backend/.env
apps/backend/test.webp
apps/backend/thumbnail-test.webp
apps/infoalloggi/.git.bkp
apps/infoalloggi/.gitignore
apps/infoalloggi/node_modules
apps/infoalloggi/.next/
apps/infoalloggi/out/
apps/infoalloggi/next-env.d.ts
apps/infoalloggi/build
apps/infoalloggi/.DS_Store
apps/infoalloggi/*.pem
apps/infoalloggi/npm-debug.log*
apps/infoalloggi/.env
apps/infoalloggi/.env*.local
apps/infoalloggi/*.tsbuildinfo
apps/infoalloggi/certificates

46
apps/backend/.air.toml Normal file
View file

@ -0,0 +1,46 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "tmp\\main.exe"
cmd = "go build -o ./tmp/main.exe ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "images", ".bin", "storage"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

View file

@ -0,0 +1,11 @@
images
storage
tmp
vscode
.air.toml
.git
.gitignore
bkp.xml
.bin
test.webp
TODO

69
apps/backend/Dockerfile Normal file
View file

@ -0,0 +1,69 @@
# Start from the latest golang base image
FROM golang:1.24 AS builder
# Set the Current Working Directory inside the container
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed
RUN go mod download
# Copy the source from the current directory to the Working Directory inside the container
COPY . .
# Build the Go app
RUN go build -o main .
# Start a new stage from alpine:latest
FROM alpine:latest AS runner
ARG POSTGRES_USER
ARG POSTGRES_PASSWORD
ARG POSTGRES_DB
ARG PGHOST
ARG PGPORT
ARG IMAGEOPTION
ARG CONCURRENT_IMAGES
ARG STORAGE_STRATEGY
ARG MINIO_ENDPOINT
ARG MINIO_PORT
ARG MINIO_ROOT_USER
ARG MINIO_ROOT_PASSWORD
RUN apk --no-cache add ca-certificates libc6-compat
ENV GOMEMLIMIT=2750MiB
ENV GOGC=100
ENV PGHOST=$PGHOST
ENV POSTGRES_DB=$POSTGRES_DB
ENV PGPORT=$PGPORT
ENV POSTGRES_USER=$POSTGRES_USER
ENV POSTGRES_PASSWORD=$POSTGRES_PASSWORD
ENV IMAGEOPTION=$IMAGEOPTION
ENV CONCURRENT_IMAGES=$CONCURRENT_IMAGES
ENV STORAGE_STRATEGY=$STORAGE_STRATEGY
ENV MINIO_ENDPOINT=$MINIO_ENDPOINT
ENV MINIO_PORT=$MINIO_PORT
ENV MINIO_ROOT_USER=$MINIO_ROOT_USER
ENV MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD
WORKDIR /app
# Copy the Pre-built binary file from the previous stage
COPY --from=builder /app/main /app/.
COPY --from=builder /app/public /app/public
COPY --from=builder /app/test.jpg /app/test.jpg
#COPY --from=builder /app/.env /app/.env
# create images and storage folder
RUN mkdir images
RUN mkdir storage
RUN mkdir storage/tmp
RUN apk add curl
# Expose port 1323 to the outside world
EXPOSE 1323
# Command to run the executable
CMD ["./main"]

4
apps/backend/TODO Normal file
View file

@ -0,0 +1,4 @@
Todo:
☐ https://medium.com/@felipedutratine/pass-environment-variables-from-docker-to-my-golang-2a967c5905fe
dsdsd

317
apps/backend/db_handler.go Normal file
View file

@ -0,0 +1,317 @@
package main
import (
"backend/dbhelpers"
"context"
"fmt"
"log/slog"
"os"
"github.com/georgysavva/scany/v2/pgxscan"
pgxuuid "github.com/jackc/pgx-gofrs-uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type DbHandler struct {
Dbpool *pgxpool.Pool
Ctx context.Context
}
func NewDbHandler() *DbHandler {
fmt.Println("Connecting to db")
ctx := context.Background()
host := os.Getenv("PGHOST")
dbname := os.Getenv("POSTGRES_DB")
dbport := os.Getenv("PGPORT")
pguser := os.Getenv("POSTGRES_USER")
pgpassword := os.Getenv("POSTGRES_PASSWORD")
connStr := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", pguser, pgpassword, host, dbport, dbname)
fmt.Printf("Connecting to db with connection string: %s", connStr)
config, err := pgxpool.ParseConfig(connStr)
if err != nil {
panic(err)
}
config.ConnConfig.Tracer = dbhelpers.NewMultiQueryTracer(dbhelpers.NewLoggingQueryTracer(slog.Default()))
config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
pgxuuid.Register(conn.TypeMap())
return nil
}
dbpool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
panic(err)
}
fmt.Println("Connected to db")
return &DbHandler{Dbpool: dbpool, Ctx: ctx}
}
func (h *DbHandler) GetActiveAnnunci() []string {
var annunci []string
pgxscan.Select(h.Ctx, h.Dbpool, &annunci, "SELECT codice FROM public.annunci WHERE web = true and stato = 'Attivo' ")
/*for i, annuncio := range annunci {
//if first letter is V then set annuncio as the last 4 digits
if string(annuncio[0]) != "V" {
annunci[i] = annuncio[:4]
}
}*/
return annunci
}
func (h *DbHandler) GetAllAnnunci() []AnnunciDB {
//var annuncio AnnunciDB
var annunci []AnnunciDB
pgxscan.Select(h.Ctx, h.Dbpool, &annunci, "SELECT * FROM public.annunci")
return annunci
}
func (h *DbHandler) InsertAnnunciInDB(annunci AnnunciParsed) {
//SET ALL WEB TO FALSE
_, err := h.Dbpool.Exec(h.Ctx, "UPDATE public.annunci SET web = false")
if err != nil {
panic(err)
}
//GET ALL CODICI
var codici []string
err = pgxscan.Select(h.Ctx, h.Dbpool, &codici, "SELECT DISTINCT codice FROM public.annunci")
if err != nil {
panic(err)
}
batch := &pgx.Batch{}
for _, annuncio := range annunci.Annuncio {
if contains(codici, annuncio.Codice) {
batch.Queue(`
UPDATE public.annunci
SET locatore = $1,
numero = $2,
idlocatore = $3,
email = $4,
creato_il = $5,
modificato_il = $6,
indirizzo = $7,
civico = $8,
comune = $9,
cap = $10,
provincia = $11,
regione = $12,
lat = $13,
lon = $14,
indirizzo_secondario = $15,
civico_secondario = $16,
lat_secondario = $17,
lon_secondario = $18,
tipo = $19,
categorie = $20,
prezzo = $21,
anno = $22,
consegna = $23,
classe = $24,
mq = $25,
piano = $26,
piano_palazzo = $27,
unita_condominio = $28,
numero_vani = $29,
numero_camere = $30,
numero_bagni = $31,
numero_balconi = $32,
numero_terrazzi = $33,
numero_box = $34,
numero_postiauto = $35,
caratteristiche = $36,
accessori = $37,
titolo_it = $38,
desc_it = $39,
titolo_en = $40,
desc_en = $41,
stato = $42,
web = $43,
homepage = $44,
url_immagini = $45,
url_video = $46
WHERE codice = $47
`, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore,
annuncio.Email, annuncio.Creato_il, annuncio.Modificato_il,
annuncio.Indirizzo, annuncio.Civico, annuncio.Comune, annuncio.Cap,
annuncio.Provincia, annuncio.Regione, annuncio.Lat, annuncio.Lon,
annuncio.Indirizzo_secondario, annuncio.Civico_secondario, annuncio.Lat_secondario,
annuncio.Lon_secondario, annuncio.Tipo, annuncio.Categorie, annuncio.Prezzo, annuncio.Anno,
annuncio.Consegna, annuncio.Classe, annuncio.Mq, annuncio.Piano, annuncio.Piano_palazzo,
annuncio.Unita_condominio, annuncio.Numero_vani, annuncio.Numero_camere, annuncio.Numero_bagni,
annuncio.Numero_balconi, annuncio.Numero_terrazzi, annuncio.Numero_box, annuncio.Numero_postiauto,
annuncio.Caratteristiche, annuncio.Accessori, annuncio.Titolo_it, annuncio.Desc_it, annuncio.Titolo_en,
annuncio.Desc_en, annuncio.Stato, annuncio.Web, annuncio.Homepage, annuncio.Url_foto,
annuncio.Url_video, annuncio.Codice)
} else {
batch.Queue(`
INSERT INTO public.annunci(
locatore,
numero,
idlocatore,
email,
creato_il,
modificato_il,
indirizzo,
civico,
comune,
cap,
provincia,
regione,
lat,
lon,
indirizzo_secondario,
civico_secondario,
lat_secondario,
lon_secondario,
tipo,
categorie,
prezzo,
anno,
consegna,
classe,
mq,
piano,
piano_palazzo,
unita_condominio,
numero_vani,
numero_camere,
numero_bagni,
numero_balconi,
numero_terrazzi,
numero_box,
numero_postiauto,
caratteristiche,
accessori,
titolo_it,
desc_it,
titolo_en,
desc_en,
stato,
web,
homepage,
url_immagini,
url_video,
codice
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47)
`, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore,
annuncio.Email, annuncio.Creato_il, annuncio.Modificato_il,
annuncio.Indirizzo, annuncio.Civico, annuncio.Comune, annuncio.Cap,
annuncio.Provincia, annuncio.Regione, annuncio.Lat, annuncio.Lon,
annuncio.Indirizzo_secondario, annuncio.Civico_secondario, annuncio.Lat_secondario,
annuncio.Lon_secondario, annuncio.Tipo, annuncio.Categorie, annuncio.Prezzo, annuncio.Anno,
annuncio.Consegna, annuncio.Classe, annuncio.Mq, annuncio.Piano, annuncio.Piano_palazzo,
annuncio.Unita_condominio, annuncio.Numero_vani, annuncio.Numero_camere, annuncio.Numero_bagni,
annuncio.Numero_balconi, annuncio.Numero_terrazzi, annuncio.Numero_box, annuncio.Numero_postiauto,
annuncio.Caratteristiche, annuncio.Accessori, annuncio.Titolo_it, annuncio.Desc_it, annuncio.Titolo_en,
annuncio.Desc_en, annuncio.Stato, annuncio.Web, annuncio.Homepage, annuncio.Url_foto,
annuncio.Url_video, annuncio.Codice)
}
}
results := h.Dbpool.SendBatch(h.Ctx, batch).Close()
fmt.Println(results)
}
func contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
}
func (h *DbHandler) ClearRecords() {
//SET ALL WEB TO FALSE
_, err := h.Dbpool.Exec(h.Ctx, "DELETE FROM public.annunci")
if err != nil {
panic(err)
}
}
func (h *DbHandler) SetFromBkp(annunci AnnunciParsed) {
h.ClearRecords()
batch := &pgx.Batch{}
for _, annuncio := range annunci.Annuncio {
batch.Queue(`
INSERT INTO public.annunci(
locatore,
numero,
idlocatore,
email,
creato_il,
modificato_il,
indirizzo,
civico,
comune,
cap,
provincia,
regione,
lat,
lon,
indirizzo_secondario,
civico_secondario,
lat_secondario,
lon_secondario,
tipo,
categorie,
prezzo,
anno,
consegna,
classe,
mq,
piano,
piano_palazzo,
unita_condominio,
numero_vani,
numero_camere,
numero_bagni,
numero_balconi,
numero_terrazzi,
numero_box,
numero_postiauto,
caratteristiche,
accessori,
titolo_it,
desc_it,
titolo_en,
desc_en,
stato,
web,
homepage,
url_immagini,
url_video,
codice
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47)
`, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore,
annuncio.Email, annuncio.Creato_il, annuncio.Modificato_il,
annuncio.Indirizzo, annuncio.Civico, annuncio.Comune, annuncio.Cap,
annuncio.Provincia, annuncio.Regione, annuncio.Lat, annuncio.Lon,
annuncio.Indirizzo_secondario, annuncio.Civico_secondario, annuncio.Lat_secondario,
annuncio.Lon_secondario, annuncio.Tipo, annuncio.Categorie, annuncio.Prezzo, annuncio.Anno,
annuncio.Consegna, annuncio.Classe, annuncio.Mq, annuncio.Piano, annuncio.Piano_palazzo,
annuncio.Unita_condominio, annuncio.Numero_vani, annuncio.Numero_camere, annuncio.Numero_bagni,
annuncio.Numero_balconi, annuncio.Numero_terrazzi, annuncio.Numero_box, annuncio.Numero_postiauto,
annuncio.Caratteristiche, annuncio.Accessori, annuncio.Titolo_it, annuncio.Desc_it, annuncio.Titolo_en,
annuncio.Desc_en, annuncio.Stato, annuncio.Web, annuncio.Homepage, annuncio.Url_foto,
annuncio.Url_video, annuncio.Codice)
}
results := h.Dbpool.SendBatch(h.Ctx, batch).Close()
fmt.Println(results)
}

View file

@ -0,0 +1,146 @@
package dbhelpers
import (
"context"
"fmt"
"log/slog"
"regexp"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// obfuscate replaces the first n characters of a string with asterisks.
func obfuscate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + strings.Repeat("*", len(s)-n)
}
// ConnectToDatabaseWithLogging connects to the database with logging. It tests
// the connection and returns the connection pool.
func ConnectToDatabaseWithLogging(ctx context.Context, connectionString string, loggingEnabled bool) (*pgxpool.Pool, error) {
// Parse the connection string
slog.
Info("parsing connection string",
slog.String("connection_string", obfuscate(connectionString, 10)),
)
config, err := pgxpool.ParseConfig(connectionString)
if err != nil {
return nil, fmt.Errorf("failed to parse connection string: %w", err)
}
// If logging is enabled, set the tracer
if loggingEnabled {
config.ConnConfig.Tracer = NewMultiQueryTracer(NewLoggingQueryTracer(slog.Default()))
}
// Create the connection pool
slog.Info("creating connection pool")
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("failed to create connection pool: %w", err)
}
// Ping the database
slog.Info("pinging database")
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return pool, nil
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
var (
replaceTabs = regexp.MustCompile(`\t+`)
replaceSpacesBeforeOpeningParens = regexp.MustCompile(`\s+\(`)
replaceSpacesAfterOpeningParens = regexp.MustCompile(`\(\s+`)
replaceSpacesBeforeClosingParens = regexp.MustCompile(`\s+\)`)
replaceSpacesAfterClosingParens = regexp.MustCompile(`\)\s+`)
replaceSpaces = regexp.MustCompile(`\s+`)
)
// prettyPrintSQL removes empty lines and trims spaces.
func prettyPrintSQL(sql string) string {
lines := strings.Split(sql, "\n")
pretty := strings.Join(lines, " ")
pretty = replaceTabs.ReplaceAllString(pretty, "")
pretty = replaceSpacesBeforeOpeningParens.ReplaceAllString(pretty, "(")
pretty = replaceSpacesAfterOpeningParens.ReplaceAllString(pretty, "(")
pretty = replaceSpacesAfterClosingParens.ReplaceAllString(pretty, ")")
pretty = replaceSpacesBeforeClosingParens.ReplaceAllString(pretty, ")")
// Finally, replace multiple spaces with a single space
pretty = replaceSpaces.ReplaceAllString(pretty, " ")
return strings.TrimSpace(pretty)
}
// https://github.com/jackc/pgx/issues/1061#issuecomment-1186250809
type LoggingQueryTracer struct {
logger *slog.Logger
}
func NewLoggingQueryTracer(logger *slog.Logger) *LoggingQueryTracer {
return &LoggingQueryTracer{logger: logger}
}
func (l *LoggingQueryTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
l.logger.
Info("query start",
slog.String("sql", prettyPrintSQL(data.SQL)),
slog.Any("args", data.Args),
)
return ctx
}
func (l *LoggingQueryTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
// Failure
if data.Err != nil {
l.logger.
Error("query end",
slog.String("error", data.Err.Error()),
slog.String("command_tag", data.CommandTag.String()),
)
return
}
// Success
l.logger.
Info("query end",
slog.String("command_tag", data.CommandTag.String()),
)
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// https://github.com/jackc/pgx/discussions/1677#discussioncomment-8815982
type MultiQueryTracer struct {
Tracers []pgx.QueryTracer
}
func NewMultiQueryTracer(tracers ...pgx.QueryTracer) *MultiQueryTracer {
return &MultiQueryTracer{Tracers: tracers}
}
func (m *MultiQueryTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
for _, t := range m.Tracers {
ctx = t.TraceQueryStart(ctx, conn, data)
}
return ctx
}
func (m *MultiQueryTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
for _, t := range m.Tracers {
t.TraceQueryEnd(ctx, conn, data)
}
}

View file

@ -0,0 +1,102 @@
services:
minio:
image: minio/minio
expose:
- "9000"
- "9001"
volumes:
- minio_storage:/data
networks:
- dokploy-network
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
command: server --console-address ":9001" /data
healthcheck:
test: [ "CMD", "curl", "-f", "http://minio:9000/minio/health/live" ]
interval: 10s
timeout: 10s
retries: 5
tiles:
image: eqalpha/keydb:latest
volumes:
- tiles-data:/data
networks:
- dokploy-network
expose:
- "6379"
command: keydb-server /etc/keydb/keydb.conf --maxmemory 100mb --maxmemory-policy volatile-lru --maxmemory-samples 5
healthcheck:
test: ["CMD", "keydb-cli", "-h", "localhost", "ping"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.5"
sysctls:
vm.overcommit_memory: "1"
keydb:
image: eqalpha/keydb:latest
volumes:
- keydb-data:/data
networks:
- dokploy-network
expose:
- "6379"
healthcheck:
test: ["CMD", "keydb-cli", "-h", "localhost", "ping"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.5"
sysctls:
vm.overcommit_memory: "1"
backend:
build:
context: .
dockerfile: Dockerfile
networks:
- dokploy-network
expose:
- 1323
restart: unless-stopped
volumes:
- images_volume:/app/images
- storage_volume:/app/storage
environment:
STORAGE_STRATEGY: "minio"
MINIO_URL: minio:9000
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGHOST: db
PGPORT: "5432"
IMAGEOPTION: "true"
CONCURRENT_IMAGES: ${CONCURRENT_IMAGES}
depends_on:
minio:
condition: service_healthy
volumes:
minio_storage:
images_volume:
storage_volume:
tiles-data:
keydb-data:
networks:
dokploy-network:
external: true

BIN
apps/backend/fallback.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

266
apps/backend/from_bkp.go Normal file
View file

@ -0,0 +1,266 @@
package main
import (
"encoding/xml"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
)
func From_Backup() XmlBkp {
//open the bkp.xml file
xmlFile, err := os.Open("bkp.xml")
if err != nil {
fmt.Println(err)
}
defer xmlFile.Close()
byteValue, err := io.ReadAll(xmlFile)
if err != nil {
fmt.Println(err)
}
var miogest XmlBkp
xml.Unmarshal(byteValue, &miogest)
return miogest
}
func ParseUpdateBkp() AnnunciParsed {
miogest := From_Backup()
var AnnunciArray AnnunciParsed
extractedData := make(chan AnnuncioParsed)
defaultCaratteristiche := Miogest.GetCaratteristiche()
defaultCategorie := Miogest.GetCategorie()
var wg sync.WaitGroup
wg.Add(len(miogest.Annunci.Record))
for i := 0; i < len(miogest.Annunci.Record); i++ {
go func(i int) {
defer wg.Done()
extractData_bkp(miogest.Annunci.Record[i], extractedData, defaultCaratteristiche, defaultCategorie)
}(i)
}
go func() {
wg.Wait()
close(extractedData)
}()
for result := range extractedData {
AnnunciArray.Annuncio = append(AnnunciArray.Annuncio, result)
}
return AnnunciArray
}
func extractData_bkp(annuncio AnnuncioBKP, extractedData chan AnnuncioParsed, defaultCaratteristiche ListaCaratteristiche, defaultCategorie []Categoria) {
var tmp AnnuncioParsed = AnnuncioParsed{}
codice := RemoveWhitespace(GetStringValue(annuncio.Codice))
tmp.Codice = codice
tmp.Email = nil
if annuncio.Proprietario != nil && annuncio.Proprietario.Cliente != nil && annuncio.Proprietario.Cliente.Nome != nil {
tmp.Locatore = annuncio.Proprietario.Cliente.Nome
}
tmp.Numero = nil
if annuncio.Proprietario != nil && annuncio.Proprietario.Cliente != nil && annuncio.Proprietario.Cliente.Nome != nil {
tmp.Idlocatore = annuncio.Proprietario.Cliente.ID
}
upperIndirizzo := MakeUppercase(GetStringValue(annuncio.Indirizzo))
tmp.Indirizzo = &upperIndirizzo
tmp.Civico = annuncio.Civico
upperComune := MakeUppercase(GetStringValue(annuncio.Comune))
tmp.Comune = &upperComune
tmp.Cap = annuncio.Cap
tmp.Provincia = annuncio.Provincia
tmp.Regione = annuncio.Regione
tmp.Lat = CommaToDot(annuncio.Lat)
tmp.Lon = CommaToDot(annuncio.Lon)
tmp.Indirizzo_secondario = nil
tmp.Civico_secondario = nil
tmp.Lat_secondario = nil
tmp.Lon_secondario = nil
var tipo = tipologiaParser(*annuncio.Tipo)
tmp.Tipo = &tipo
tmpanno := GetStringValue(annuncio.Anno)
if tmpanno != "0" && tmpanno != "" {
tmp.Anno = &tmpanno
}
tmp.Prezzo = prezzoParse(annuncio.Prezzo)
tmp.Consegna = parseConsegna(annuncio.Consegna)
tmp.Classe = annuncio.Classe
if annuncio.Categorie != nil && annuncio.Categorie.Categoria != nil {
resultArray := make([]string, 0)
cats := *annuncio.Categorie.Categoria
for _, cat := range cats {
for _, catdef := range defaultCategorie {
if cat.ID == catdef.Id {
resultArray = append(resultArray, catdef.Nome)
}
}
}
if tmp.Categorie == nil {
tmp.Categorie = &[]string{}
}
*tmp.Categorie = resultArray
}
tmp.Mq = CommaToDot(annuncio.Mq)
tmp.Piano = annuncio.Piano
tmp.Piano_palazzo = annuncio.PianoPalazzo
tmp.Unita_condominio = annuncio.UnitaImmobiliari
tmp.Numero_vani = annuncio.NumeroVani
tmp.Numero_camere = annuncio.NumeroCamere
tmp.Numero_bagni = annuncio.NumeroBagni
tmp.Numero_balconi = annuncio.NumeroBalconi
tmp.Numero_terrazzi = annuncio.NumeroTerrazzi
tmp.Numero_box = annuncio.NumeroBox
tmp.Numero_postiauto = annuncio.NumeroPostiauto
tmpstato := GetStringValue(annuncio.Stato)
var tmpstatoString string
if tmpstato == "1" {
tmpstatoString = "Attivo"
}
if tmpstato == "2" {
tmpstatoString = "Trattativa"
} else {
tmpstatoString = "Sospeso"
}
tmp.Stato = &tmpstatoString
home := false
tmp.Homepage = &home
layout := "20060102150405"
if annuncio.Inserito != nil {
t, err := time.Parse(layout, *annuncio.Inserito)
if err != nil {
tmp.Creato_il = nil
} else {
tmp.Creato_il = &t
}
}
tmp.Modificato_il = nil
var itlingua string = "it"
var enlingua string = "en"
var tmpTitoloIt *string = nil
var tmpTitoloEn *string = nil
var tmpDescIt *string = nil
var tmpDescEn *string = nil
if annuncio.Descrizioni != nil {
for _, desc := range *annuncio.Descrizioni.Descrizione {
if desc.Lingua != nil {
if *desc.Lingua == itlingua {
tmpTitoloIt = desc.Titolo
tmpDescIt = desc.Testo
}
if *desc.Lingua == enlingua {
tmpTitoloEn = desc.Titolo
tmpDescEn = desc.Testo
}
}
}
}
tmp.Titolo_it = tmpTitoloIt
tmp.Titolo_en = tmpTitoloEn
tmp.Desc_it = tmpDescIt
tmp.Desc_en = tmpDescEn
w := false
tmp.Web = &w
tmp.Url_video = nil
if annuncio.Accessori != nil && annuncio.Accessori.Accessorio != nil {
tmpAcc := make([]string, 0)
for _, acc := range *annuncio.Accessori.Accessorio {
if acc.Nome != nil {
tmpAcc = append(tmpAcc, *acc.Nome)
}
}
if tmp.Accessori == nil {
tmp.Accessori = &[]string{}
}
*tmp.Accessori = tmpAcc
}
tmpCaratteristiche := make(map[string]interface{})
if annuncio.Schede != nil && annuncio.Schede.Scheda != nil {
for _, scheda := range *annuncio.Schede.Scheda {
if scheda.Valore != nil {
var cossriponding_name *string = nil
for _, def := range defaultCaratteristiche.Caratteristiche {
if scheda.ID == def.Id {
cossriponding_name = &def.Tagxml
}
}
if cossriponding_name != nil {
tmpCaratteristiche[*cossriponding_name] = *scheda.Valore
}
}
}
}
for _, def := range defaultCaratteristiche.Caratteristiche {
if _, ok := tmpCaratteristiche[def.Tagxml]; !ok {
tmpCaratteristiche[def.Tagxml] = nil
}
}
tmp.Caratteristiche = &tmpCaratteristiche
tmp.Url_foto = nil
tmp.Url_video = nil
extractedData <- tmp
}
func TestTipologieBkp() {
bkp := From_Backup()
var tipologie []string
for _, r := range bkp.Annunci.Record {
if r.Tipo != nil {
if !contains(tipologie, *r.Tipo) {
tipologie = append(tipologie, *r.Tipo)
}
}
}
fmt.Println(strings.Join(tipologie, ","))
}
func tipologiaParser(tipo string) string {
var result string
switch tipo {
case "Affitto Residenziale", "Affitto Turistico":
result = "Transitorio"
case "Vendita Residenziale", "Vendita Commerciale":
result = "Vendita"
case "Affitto Commerciale", "Affitto":
result = "Stabile"
case "Cessione Commerciale", "Cessione":
result = "Cessione"
default:
result = "Errore"
}
return result
}

55
apps/backend/go.mod Normal file
View file

@ -0,0 +1,55 @@
module backend
go 1.22
require (
github.com/georgysavva/scany/v2 v2.1.3
github.com/jackc/pgx/v5 v5.5.5
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.11.2
)
require (
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/frankban/quicktest v1.14.6 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gofrs/uuid/v5 v5.0.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mholt/archiver v3.1.1+incompatible // indirect
github.com/minio/crc64nvme v1.0.0 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/image v0.14.0 // indirect
golang.org/x/sync v0.11.0 // indirect
)
require (
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/minio/minio-go/v7 v7.0.86
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
)

137
apps/backend/go.sum Normal file
View file

@ -0,0 +1,137 @@
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/georgysavva/scany/v2 v2.1.3 h1:Zd4zm/ej79Den7tBSU2kaTDPAH64suq4qlQdhiBeGds=
github.com/georgysavva/scany/v2 v2.1.3/go.mod h1:fqp9yHZzM/PFVa3/rYEC57VmDx+KDch0LoqrJzkvtos=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 h1:QWdhlQz98hUe1xmjADOl2mr8ERLrOqj0KWLdkrnNsRQ=
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2/go.mod h1:Ti7pyNDU/UpXKmBTeFgxTvzYDM9xHLiYKMsLdt4b9cg=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE=
github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
github.com/minio/crc64nvme v1.0.0 h1:MeLcBkCTD4pAoU7TciAfwsfxgkhM2u5hCe48hSEVFr0=
github.com/minio/crc64nvme v1.0.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.86 h1:DcgQ0AUjLJzRH6y/HrxiZ8CXarA70PAIufXHodP4s+k=
github.com/minio/minio-go/v7 v7.0.86/go.mod h1:VbfO4hYwUu3Of9WqGLBZ8vl3Hxnxo4ngxK4hzQDf4x4=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84 h1:/6MoQlTdk1eAi0J9O89ypO8umkp+H7mpnSF2ggSL62Q=
github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84/go.mod h1:Eeech2fhQ/E4bS8cdc3+SGABQ+weQYGyWBvZ/mNr5uY=
github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1 h1:9awJsNP+gYOGCr3pQu9i217bCNsVwoQCmD3h7CYwxOw=
github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1/go.mod h1:m5oz0fmp+uyRBxxFkvciIpe1wd2JZ3pDVJ3x/D8/EGw=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,127 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
)
type ImageRefManager struct {
}
func NewImageRefManager() *ImageRefManager {
return &ImageRefManager{}
}
func (i *ImageRefManager) GetImagesRef() ([]MiogestImagesRef_Table, error) {
var images []MiogestImagesRef_Table
err := pgxscan.Select(Db.Ctx, Db.Dbpool, &images, "SELECT * FROM public.miogest_images_ref")
if err != nil {
return nil, err
}
return images, nil
}
func (i *ImageRefManager) InsertImagesRef(annunci *AnnunciXML) error {
batch := &pgx.Batch{}
for _, annuncio := range annunci.Annuncio {
batch.Queue(`
INSERT INTO public.miogest_images_ref (codice, foto)
VALUES ($1, $2)
ON CONFLICT (codice) DO UPDATE SET foto = $2
`, &annuncio.Codice, &annuncio.Foto)
}
if err := Db.Dbpool.SendBatch(Db.Ctx, batch).Close(); err != nil {
return err
}
return nil
}
func (i *ImageRefManager) FindIsUpdate(annunci *AnnunciXML) ([]string, error) {
r, err := i.GetImagesRef()
if err != nil {
return nil, err
}
refMap := make(map[string][]string) // Convert slice to map for faster lookup
for _, ref := range r {
if ref.Foto != nil {
refMap[ref.Codice] = *ref.Foto
}
}
var result []string
for _, annuncio := range annunci.Annuncio {
if annuncio.Codice != nil && annuncio.Foto != nil {
refFoto, exists := refMap[*annuncio.Codice]
if !exists {
fmt.Println("new codice", *annuncio.Codice)
result = append(result, *annuncio.Codice)
continue
}
if len(*annuncio.Foto) != len(refFoto) {
fmt.Println("Different length", *annuncio.Foto, refFoto)
result = append(result, *annuncio.Codice)
continue
}
for i, foto := range *annuncio.Foto {
if foto != refFoto[i] {
fmt.Println("Different content", *annuncio.Foto, refFoto)
result = append(result, *annuncio.Codice)
break
}
}
}
}
return result, nil
}
func (i *ImageRefManager) ClearImages(to_clear []string) error {
for _, codice := range to_clear {
folder := filepath.Join(imgbasepath, codice)
err := os.RemoveAll(folder)
if err != nil {
return err
}
}
return nil
}
func (i *ImageRefManager) Reset() error {
_, err := Db.Dbpool.Exec(Db.Ctx, "DELETE FROM public.miogest_images_ref")
if err != nil {
return err
}
_, err = Db.Dbpool.Exec(Db.Ctx, "UPDATE public.annunci SET url_immagini = NULL, url_video = NULL")
if err != nil {
return err
}
err = os.RemoveAll(imgbasepath)
if err != nil {
log.Fatal(err)
}
err = os.Mkdir(imgbasepath, 0755)
if err != nil {
log.Fatal(err)
}
return nil
}

206
apps/backend/images.go Normal file
View file

@ -0,0 +1,206 @@
package main
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"image/png"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/labstack/echo/v4"
"github.com/nfnt/resize"
"github.com/nickalie/go-webpbin"
)
func Conversion(input string) error {
var width uint = 1920
output := filepath.Base(input)
outputdir := filepath.Dir(input)
ext := filepath.Ext(output)
output = output[0 : len(output)-len(ext)] // Remove file extension // Add .webp extension
log.Println("Converting", input, "to", output+".webp")
f, err := os.Open(input)
if err != nil {
return err
}
defer f.Close()
var img image.Image
switch ext {
case ".webp":
log.Println("Image already in WebP format")
img, err = webpbin.Decode(f)
if err != nil {
return err
}
case ".png", ".PNG":
// Convert from PNG
img, err = png.Decode(f)
if err != nil {
return err
}
case ".jpeg", ".jpg", ".JPG", ".JPEG":
// Convert from JPEG
img, err = jpeg.Decode(f)
if err != nil {
return err
}
default:
return fmt.Errorf("error: Unsupported file type: %s", ext)
}
err = image_creation(img, width, output, outputdir)
if err != nil {
return err
}
err = thumbnail_creation(img, output, outputdir)
if err != nil {
return err
}
fmt.Println("Conversion completed successfully")
return nil
}
func image_creation(img image.Image, width uint, output string, outputdir string) error {
// Resize the image
resizedImg := resize.Resize(width, 0, img, resize.Lanczos3)
// Create the output file
outputPath := fmt.Sprintf("%s/%s.webp", outputdir, output)
log.Println("Creating", outputPath)
out, err := os.Create(outputPath)
if err != nil {
return err
}
defer out.Close()
// Encode the resized image to the output file
err = webpbin.Encode(out, resizedImg)
if err != nil {
return err
}
return nil
}
func thumbnail_creation(img image.Image, output string, outputdir string) error {
// Resize the image
resizedImg := resize.Resize(32, 0, img, resize.Lanczos3)
// Create the output file
outputPath := fmt.Sprintf("%s/thumbnail-%s.webp", outputdir, output)
log.Println("Creating", outputPath)
out, err := os.Create(outputPath)
if err != nil {
return err
}
defer out.Close()
// Encode the resized image to the output file
err = webpbin.Encode(out, resizedImg)
if err != nil {
return err
}
return nil
}
func ImageRoutes(e *echo.Echo) {
e.GET("/initcwebp", func(c echo.Context) error {
Conversion("test.jpg")
return c.String(http.StatusOK, "Cwebp initialized")
})
e.GET("/images/list", func(c echo.Context) error {
annunci := Db.GetActiveAnnunci()
return c.JSON(http.StatusOK, annunci)
})
e.GET("/images/get/:cod/:imgNum", func(c echo.Context) error {
cod := c.Param("cod")
imgNum := c.Param("imgNum")
thumb := c.QueryParam("thumbnail") == "true"
widthStr := c.QueryParam("w")
//quality := c.QueryParam("q")
fmt.Printf("request for %s/%s, thumbnail=%t, w=%s\n", cod, imgNum, thumb, widthStr)
prefix := ""
if thumb {
prefix = "thumbnail-"
}
Path := filepath.Join(imgbasepath, cod, imgNum, fmt.Sprintf("%s%s_%s.webp", prefix, cod, imgNum))
fmt.Println(Path)
if _, err := os.Stat(Path); os.IsNotExist(err) {
fmt.Println("Image does not exist:", Path)
// Serve a fallback image if the requested image is not found
return FallbackImage(c)
}
if widthStr != "" {
width, err := strconv.Atoi(widthStr)
if err != nil {
fmt.Println("Invalid width parameter:", widthStr)
return FallbackImage(c)
}
f, err := os.Open(Path)
if err != nil {
return err
}
img, err := webpbin.Decode(f)
if err != nil {
fmt.Println("Failed to decode image:", err)
return FallbackImage(c)
}
resizedImg := resize.Resize(uint(width), 0, img, resize.Lanczos3)
buffer := new(bytes.Buffer)
err = webpbin.Encode(buffer, resizedImg)
if err != nil {
fmt.Println("Failed to encode image:", err)
return FallbackImage(c)
}
return c.Blob(http.StatusOK, "image/webp", buffer.Bytes())
}
return c.File(Path)
})
e.GET("/images/resetimages", func(c echo.Context) error {
err := ImageManager.Reset()
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
return c.String(http.StatusOK, "OK")
})
e.POST("/image-option-toggle", func(c echo.Context) error {
// Toggle the imageOption value
imageOption = !imageOption
// Convert the boolean value to a string and set the environment variable
os.Setenv("IMAGEOPTION", strconv.FormatBool(imageOption))
return c.String(http.StatusOK, "OK")
})
}
func FallbackImage(c echo.Context) error {
// Serve a fallback image if the requested image is not found
fallbackPath := "fallback.webp" // Path to your fallback image
if _, err := os.Stat(fallbackPath); os.IsNotExist(err) {
return c.String(http.StatusNotFound, "Fallback image not found")
}
return c.File(fallbackPath)
}

View file

@ -0,0 +1,516 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"time"
)
type MiogestHandler struct {
Caratterisiche ListaCaratteristiche
Categorie []Categoria
AnnunciXML AnnunciXML
AnnunciParsed AnnunciParsed
annunci_cache_expires time.Time
vars_cache_expires time.Time
}
func NewMiogestHandler() *MiogestHandler {
return &MiogestHandler{
Caratterisiche: ListaCaratteristiche{},
Categorie: nil,
AnnunciXML: AnnunciXML{},
AnnunciParsed: AnnunciParsed{},
annunci_cache_expires: time.Now(),
vars_cache_expires: time.Now(),
}
}
// ANNUNCI FROM MIOGEST
func (m *MiogestHandler) GetAnnunci() AnnunciXML {
if len(m.AnnunciXML.Annuncio) == 0 || time.Now().After(m.annunci_cache_expires) {
m.GetAnnunciFromMiogest()
}
return m.AnnunciXML
}
func (m *MiogestHandler) GetAnnunciFromMiogest() {
m.AnnunciXML = GetDataXML[AnnunciXML]("http://partner.miogest.com/agenzie/infoalloggi.xml")
m.annunci_cache_expires = time.Now().Add(1 * time.Hour)
}
// CATEGORIE AND CARATTERISTICHE FROM MIOGEST
func (m *MiogestHandler) InitDefaults() {
m.GenerateCaratteristiche()
m.GenerateCategorie()
}
func (m *MiogestHandler) GetCaratteristiche() ListaCaratteristiche {
if len(m.Caratterisiche.Caratteristiche) == 0 || time.Now().After(m.vars_cache_expires) {
m.GenerateCaratteristiche()
}
return m.Caratterisiche
}
func (m *MiogestHandler) GetCategorie() []Categoria {
if len(m.Categorie) == 0 || time.Now().After(m.vars_cache_expires) {
m.GenerateCategorie()
}
return m.Categorie
}
func (m *MiogestHandler) GenerateCaratteristiche() {
var result = GetDataXML[DefaultCaratteristiche]("https://www.miogest.com/apps/revo.aspx?tipo=schede")
var array ListaCaratteristiche
for _, tag := range result.Scheda {
var c = Caratteristica{Id: tag.Id, Tagxml: tag.Tagxml}
array.Caratteristiche = append(array.Caratteristiche, c)
}
m.Caratterisiche = array
m.vars_cache_expires = time.Now().Add(1 * time.Hour)
}
func (m *MiogestHandler) GenerateCategorie() {
var result = GetDataXML[DefaultCategories]("https://www.miogest.com/apps/revo.aspx?tipo=categorie")
var listaCategorie []Categoria
for _, elem := range result.Cat {
var c = Categoria{Id: elem.ID, Nome: elem.Nome}
listaCategorie = append(listaCategorie, c)
}
m.Categorie = listaCategorie
m.vars_cache_expires = time.Now().Add(1 * time.Hour)
}
// ANNUNCI PARSED
func (m *MiogestHandler) GetAnnunciParsed() AnnunciParsed {
m.ParseAnnunci()
return m.AnnunciParsed
}
func (m *MiogestHandler) ParseAnnunci() {
annunci := m.GetAnnunci()
cod_image_to_update := []string{}
if imageOption {
var err error
cod_image_to_update, err = ImageManager.FindIsUpdate(&annunci)
if err != nil {
log.Fatalf("Failed to find images to update: %v", err.Error())
}
if err := ImageManager.ClearImages(cod_image_to_update); err != nil {
log.Fatalf("Failed to clear images: %v", err.Error())
}
ImageManager.InsertImagesRef(&annunci)
}
var AnnunciArray AnnunciParsed
AnnunciArray.Annuncio = make([]AnnuncioParsed, 0, len(annunci.Annuncio))
Miogest.InitDefaults()
var wg sync.WaitGroup
wg.Add(len(annunci.Annuncio))
sem := make(chan struct{}, concurrentImages) //Limit concurrent goroutines
lista_codici := []string{}
for i := range annunci.Annuncio {
lista_codici = append(lista_codici, RemoveWhitespace(*annunci.Annuncio[i].Codice))
sem <- struct{}{}
go func(i int) {
defer wg.Done()
defer func() { <-sem }()
result := extractData_update(&annunci.Annuncio[i], contains(cod_image_to_update, *annunci.Annuncio[i].Codice))
AnnunciArray.Annuncio = append(AnnunciArray.Annuncio, result)
}(i)
}
wg.Wait()
if imageOption {
for _, codice := range lista_codici {
err := folderCleanup(codice)
if err != nil {
log.Fatalf("Failed to cleanup folder: %v", err.Error())
}
}
}
m.AnnunciParsed = AnnunciArray
}
var re = regexp.MustCompile(`[^+\d]`)
func boolUpd(b bool) *bool {
return &b
}
func strUpd(s string) *string {
return &s
}
func extractData_update(annuncio *AnnuncioXML, updateImages bool) AnnuncioParsed {
var tmp AnnuncioParsed = AnnuncioParsed{}
tmp.Codice = RemoveWhitespace(GetStringValue(annuncio.Codice))
processLocatore(&tmp, annuncio)
processIndirizzo(&tmp, annuncio)
processTipologia(&tmp, annuncio)
tmp.Consegna = parseConsegna(annuncio.Consegna)
processCategorie(&tmp, annuncio)
processDatiImmobile(&tmp, annuncio)
tmp.Stato = annuncio.Stato
tmp.Homepage = boolUpd(false)
if annuncio.HomePage != nil {
if *annuncio.HomePage == "si" {
tmp.Homepage = boolUpd(true)
}
}
layout := "20060102150405"
if annuncio.Creato != nil {
t, err := time.Parse(layout, *annuncio.Creato)
if err != nil {
tmp.Creato_il = nil
} else {
tmp.Creato_il = &t
}
}
if annuncio.Modifica != nil {
t, err := time.Parse(layout, *annuncio.Modifica)
if err != nil {
tmp.Modificato_il = nil
} else {
tmp.Modificato_il = &t
}
}
tmp.Titolo_it = annuncio.Titolo
tmp.Titolo_en = annuncio.TitoloEn
tmp.Desc_it = annuncio.Descrizione
tmp.Desc_en = annuncio.DescrizioneEn
tmp.Web = boolUpd(true)
videos := []string{}
if annuncio.Video != nil {
videos = *annuncio.Video
}
miogestVideos := []string{}
if annuncio.AMMedias != nil {
for _, media := range *annuncio.AMMedias {
if media.AMVideo != nil && media.AMVideo.Url != nil {
miogestVideos = append(miogestVideos, *media.AMVideo.Url)
}
}
}
concatenatedVideos := slices.Concat(videos, miogestVideos)
tmp.Url_video = &concatenatedVideos
if annuncio.Accessori != nil {
tmp.Accessori = annuncio.Accessori
}
tmpcaratt := parseCaratteristiche(annuncio)
tmp.Caratteristiche = &tmpcaratt
if updateImages {
processImages(&tmp, annuncio)
}
folders := GetPathDirsArray(filepath.Join(imgbasepath, tmp.Codice))
for _, folder := range folders {
folderpath := filepath.Join(filepath.Join(imgbasepath, tmp.Codice), folder)
files, err := os.ReadDir(folderpath)
if err != nil {
panic(err)
}
for _, file := range files {
if strings.Contains(file.Name(), "webp") && !strings.HasPrefix(file.Name(), "thumbnail") {
tmpImgUrl := tmp.Codice + "/" + folder
if tmp.Url_foto == nil {
tmp.Url_foto = &[]string{}
}
*tmp.Url_foto = append(*tmp.Url_foto, tmpImgUrl)
}
}
}
return tmp
}
func processLocatore(tmp *AnnuncioParsed, annuncio *AnnuncioXML) {
if annuncio.Clienti == nil || annuncio.Clienti.Cliente == nil {
return
}
tmp.Email = annuncio.Clienti.Cliente.Email1
var number string
for _, tel := range []*string{annuncio.Clienti.Cliente.Tel1, annuncio.Clienti.Cliente.Tel2, annuncio.Clienti.Cliente.Tel3} {
if tel != nil {
cleanedTel := re.ReplaceAllString(*tel, "")
if cleanedTel != "" {
number = cleanedTel
break
}
}
}
tmp.Numero = &number
if annuncio.Clienti.Cliente.Cognome != nil && annuncio.Clienti.Cliente.Nome != nil {
tmp.Locatore = strUpd(fmt.Sprintf("%s %s", MakeUppercase(*annuncio.Clienti.Cliente.Cognome), MakeUppercase(*annuncio.Clienti.Cliente.Nome)))
}
tmp.Idlocatore = annuncio.Clienti.Cliente.Id
}
func processIndirizzo(tmp *AnnuncioParsed, annuncio *AnnuncioXML) {
tmp.Indirizzo = strUpd(MakeUppercase(GetStringValue(annuncio.Indirizzo)))
tmp.Civico = annuncio.Civico
tmp.Comune = strUpd(MakeUppercase(GetStringValue(annuncio.Comune)))
tmp.Cap = annuncio.Cap
tmp.Provincia = annuncio.Provincia
tmp.Regione = annuncio.Regione
tmp.Lat = annuncio.Latitudine
tmp.Lon = annuncio.Longitudine
tmp.Indirizzo_secondario = strUpd(MakeUppercase(GetStringValue(annuncio.IndirizzoSecondario)))
tmp.Civico_secondario = annuncio.CivicoSecondario
tmp.Lat_secondario = annuncio.LatitudineSecondario
tmp.Lon_secondario = annuncio.LongitudineSecondario
}
func processTipologia(tmp *AnnuncioParsed, annuncio *AnnuncioXML) {
if annuncio.Tipologia == nil {
tmp.Tipo = strUpd("Errore")
return
}
if *annuncio.Tipologia == "V" {
tmp.Tipo = strUpd("Vendita")
return
}
if *annuncio.Tipologia == "I" {
tmp.Tipo = strUpd("Cessione")
return
}
if *annuncio.Tipologia == "A" {
if *annuncio.Tipologia2 == "Residenziale" || *annuncio.Tipologia2 == "Turistico" || *annuncio.Tipologia2 == "Stanza" {
tmp.Tipo = strUpd("Transitorio")
} else {
tmp.Tipo = strUpd("Stabile")
}
return
}
tmp.Tipo = strUpd("Errore")
}
func processCategorie(tmp *AnnuncioParsed, annuncio *AnnuncioXML) {
if annuncio.Categoria == nil {
return
}
resultArray := make([]string, 0)
cats := *annuncio.Categoria
defaultCategorie := Miogest.GetCategorie()
for _, cat := range cats {
for _, catdef := range defaultCategorie {
if cat == catdef.Id {
resultArray = append(resultArray, catdef.Nome)
}
}
}
if tmp.Categorie == nil {
tmp.Categorie = &[]string{}
}
*tmp.Categorie = resultArray
}
func prezzoParse(prezzo *string) int {
zero := 0
if prezzo == nil {
return zero
}
f, err := strconv.ParseFloat(strings.TrimSpace(strings.ReplaceAll(*prezzo, ",", ".")), 64)
if err != nil {
log.Printf("Failed to parse price: %v", err)
return zero
}
prezzoInt := int(f * 100) // Convert to cents
return prezzoInt
}
func processDatiImmobile(tmp *AnnuncioParsed, annuncio *AnnuncioXML) {
tmpanno := GetStringValue(annuncio.Anno)
if tmpanno != "0" && tmpanno != "" {
tmp.Anno = &tmpanno
}
tmp.Prezzo = prezzoParse(annuncio.Prezzo)
tmp.Classe = annuncio.Classe
if annuncio.Mq != nil {
replaced := strings.ReplaceAll(*annuncio.Mq, ",", "")
tmp.Mq = &replaced
}
tmp.Piano = annuncio.Piano
tmp.Piano_palazzo = annuncio.PianiCondominio
tmp.Unita_condominio = annuncio.UnitaCondominio
tmp.Numero_vani = annuncio.Vani
tmp.Numero_camere = annuncio.Camere
tmp.Numero_bagni = annuncio.Bagni
tmp.Numero_balconi = annuncio.Balconi
tmp.Numero_terrazzi = annuncio.Terrazzi
tmp.Numero_box = annuncio.Box
tmp.Numero_postiauto = annuncio.PostiAuto
}
func processImages(tmp *AnnuncioParsed, annuncio *AnnuncioXML) {
path := filepath.Join(imgbasepath, tmp.Codice)
err := os.MkdirAll(path, 0755)
if err != nil {
log.Printf("Failed to create directory: %v", err)
panic(err)
}
var wg sync.WaitGroup
sem := make(chan struct{}, concurrentImages) // Limit concurrent goroutines
nfoto := 0
var fotos []string
if annuncio.Foto != nil {
fotos = *annuncio.Foto
}
for _, url := range fotos {
fmt.Println("Processing image", url)
imgpath := filepath.Join(path, fmt.Sprintf("%d", nfoto))
if _, err := os.Stat(imgpath); os.IsNotExist(err) {
os.Mkdir(imgpath, 0755)
}
filename := filepath.Join(imgpath, fmt.Sprintf("%s_%d%s", tmp.Codice, nfoto, filepath.Ext(url)))
wg.Add(1)
sem <- struct{}{}
go func(url, filename string) {
defer wg.Done()
defer func() { <-sem }()
downloadImage(url, filename)
}(url, filename)
nfoto++
}
wg.Wait()
processWebpImages(path)
}
func downloadImage(url, filename string) {
response, err := http.Get(url)
if err != nil {
log.Fatalf("Failed to download image: %v", err)
return
}
defer response.Body.Close()
file, err := os.Create(filename)
if err != nil {
log.Fatalf("Failed to create file: %v", err)
return
}
defer file.Close()
_, err = io.Copy(file, response.Body)
if err != nil {
log.Fatalf("Failed to save image: %v", err)
}
}
func processWebpImages(path string) {
array, err := GetPathsArray(path)
if err != nil {
log.Fatal(err)
}
var wg sync.WaitGroup
sem := make(chan struct{}, concurrentImages) // Limit concurrent goroutines
for _, img := range array {
wg.Add(1)
sem <- struct{}{}
go func(img string) {
defer wg.Done()
defer func() { <-sem }()
err := Conversion(img)
if err != nil {
log.Fatal(err)
}
}(img)
}
wg.Wait()
}
func folderCleanup(codice string) error {
folders := GetPathDirsArray(filepath.Join(imgbasepath, codice))
non_webp := []string{}
for _, folder := range folders {
folderpath := filepath.Join(imgbasepath, codice, folder)
files, err := os.ReadDir(folderpath)
if err != nil {
return err
}
for _, file := range files {
if !strings.Contains(file.Name(), "webp") {
non_webp = append(non_webp, filepath.Join(folderpath, file.Name()))
}
}
}
for _, file := range non_webp {
err := os.Remove(file)
if err != nil {
return err
}
}
return nil
}
func parseCaratteristiche(wrkrecord *AnnuncioXML) map[string]interface{} {
if reflect.TypeOf(wrkrecord).Kind() != reflect.Ptr || reflect.ValueOf(wrkrecord).Elem().Kind() != reflect.Struct {
return nil
}
tmpCaratteristiche := make(map[string]interface{})
val := reflect.ValueOf(wrkrecord).Elem()
numField := val.NumField()
for i := 0; i < numField; i++ {
if strings.HasPrefix(val.Type().Field(i).Name, "Scheda_") {
nameField := val.Type().Field(i).Name
valueField := val.Field(i)
if valueField.IsValid() && !valueField.IsNil() {
tmpCaratteristiche[nameField] = valueField.Interface()
} else {
tmpCaratteristiche[nameField] = nil
}
}
}
for _, scheda := range Miogest.GetCaratteristiche().Caratteristiche {
if _, ok := tmpCaratteristiche[scheda.Tagxml]; !ok {
tmpCaratteristiche[scheda.Tagxml] = nil
}
}
return tmpCaratteristiche
}
var mesiMapping = map[int]string{
1: "Gennaio",
2: "Febbraio",
3: "Marzo",
4: "Aprile",
5: "Maggio",
6: "Giugno",
7: "Luglio",
8: "Agosto",
9: "Settembre",
10: "Ottobre",
11: "Novembre",
12: "Dicembre",
}
func parseConsegna(original *string) *int {
currentMonth := int(time.Now().Month())
if original == nil {
return nil
}
if *original == "Libero" {
return &currentMonth
} else {
for k, v := range mesiMapping {
if *original == v {
return &k
}
}
}
return &currentMonth
}

365
apps/backend/models.go Normal file
View file

@ -0,0 +1,365 @@
package main
import (
"os"
"time"
)
type XmlBkp struct {
Annunci struct {
Record []AnnuncioBKP `xml:"record"`
} `xml:"annunci"`
}
type AnnuncioBKP struct {
Codice *string `xml:"codice"`
Proprietario *struct {
Cliente *struct {
ID *string `xml:"id"`
Nome *string `xml:"nome"`
} `xml:"cliente"`
} `xml:"proprietario"`
Nazione *string `xml:"nazione"`
Regione *string `xml:"regione"`
Provincia *string `xml:"provincia"`
Comune *string `xml:"comune"`
Cap *string `xml:"cap"`
Indirizzo *string `xml:"indirizzo"`
Civico *string `xml:"civico"`
Lat *string `xml:"lat"`
Lon *string `xml:"lon"`
Tipo *string `xml:"tipo"`
Categorie *struct {
Categoria *[]struct {
ID string `xml:"id"`
Nome string `xml:"nome"`
} `xml:"categoria"`
} `xml:"categorie"`
Prezzo *string `xml:"prezzo"`
Piani *string `xml:"piani"`
Anno *string `xml:"anno"`
Consegna *string `xml:"consegna"`
Classe *string `xml:"classe"`
Mq *string `xml:"mq"`
Piano *string `xml:"piano"`
PianoPalazzo *string `xml:"piano_palazzo"`
UnitaImmobiliari *string `xml:"unita_immobiliari"`
NumeroVani *string `xml:"numero_vani"`
NumeroCamere *string `xml:"numero_camere"`
NumeroLetti *string `xml:"numero_letti"`
NumeroBagni *string `xml:"numero_bagni"`
NumeroBalconi *string `xml:"numero_balconi"`
NumeroTerrazzi *string `xml:"numero_terrazzi"`
NumeroBox *string `xml:"numero_box"`
NumeroPostiauto *string `xml:"numero_postiauto"`
Schede *struct {
Scheda *[]struct {
ID string `xml:"id"`
Nome string `xml:"nome"`
Valore *string `xml:"valore"`
} `xml:"scheda"`
} `xml:"schede"`
Accessori *struct {
Accessorio *[]struct {
ID string `xml:"id"`
Nome *string `xml:"nome"`
} `xml:"accessorio"`
} `xml:"accessori"`
Web *string `xml:"web"`
Descrizioni *struct {
Descrizione *[]struct {
Lingua *string `xml:"lingua"`
Titolo *string `xml:"titolo"`
Testo *string `xml:"testo"`
} `xml:"descrizione"`
} `xml:"descrizioni"`
Allegati *struct {
Foto *[]struct {
Nome *string `xml:"nome"`
URL *string `xml:"url"`
} `xml:"foto"`
} `xml:"allegati"`
Inserito *string `xml:"inserito"`
Stato *string `xml:"stato"`
}
type AnnunciXML struct {
Annuncio []AnnuncioXML `xml:"Annuncio"`
}
type AnnuncioXML struct {
Codice *string `xml:"Codice"`
Stato *string `xml:"Stato"`
Creato *string `xml:"Creato"`
Modifica *string `xml:"Modifica"`
Regione *string `xml:"Regione"`
Provincia *string `xml:"Provincia"`
Comune *string `xml:"Comune"`
Indirizzo *string `xml:"Indirizzo"`
Civico *string `xml:"Civico"`
Latitudine *string `xml:"Latitudine"`
Longitudine *string `xml:"Longitudine"`
IndirizzoSecondario *string `xml:"IndirizzoSecondario"`
CivicoSecondario *string `xml:"CivicoSecondario"`
LatitudineSecondario *string `xml:"LatitudineSecondario"`
LongitudineSecondario *string `xml:"LongitudineSecondario"`
Tipologia *string `xml:"Tipologia"`
Tipologia2 *string `xml:"Tipologia2"`
Categoria *[]string `xml:"Categoria"`
HomePage *string `xml:"HomePage"`
Prezzo *string `xml:"Prezzo"`
Anno *string `xml:"Anno"`
Mq *string `xml:"Mq"`
Vani *string `xml:"Vani"`
Camere *string `xml:"Camere"`
Bagni *string `xml:"Bagni"`
Balconi *string `xml:"Balconi"`
Terrazzi *string `xml:"Terrazzi"`
Piano *string `xml:"Piano"`
PianiCondominio *string `xml:"PianiCondominio"`
UnitaCondominio *string `xml:"UnitaCondominio"`
Box *string `xml:"Box"`
PostiAuto *string `xml:"PostiAuto"`
Classe *string `xml:"Classe"`
Scheda_AccessoDisabili *string `xml:"Scheda_AccessoDisabili"`
Scheda_AccoglienzaBimbi *string `xml:"Scheda_AccoglienzaBimbi"`
Scheda_AcquaCalda *string `xml:"Scheda_AcquaCalda"`
Scheda_AffittoMuriAnno *string `xml:"Scheda_AffittoMuriAnno"`
Scheda_Appartamenti *string `xml:"Scheda_Appartamenti"`
Scheda_Arredi *string `xml:"Scheda_Arredi"`
Scheda_Balconi *string `xml:"Scheda_Balconi"`
Scheda_CambioBiancheria *string `xml:"Scheda_CambioBiancheria"`
Scheda_CanoniAnticipati *string `xml:"Scheda_CanoniAnticipati"`
Scheda_CantinaSolaio *string `xml:"Scheda_CantinaSolaio"`
Scheda_Categoria *string `xml:"Scheda_Categoria"`
Scheda_Cauzione *string `xml:"Scheda_Cauzione"`
Scheda_Destinazione *string `xml:"Scheda_Destinazione"`
Scheda_ElettricitaGas *string `xml:"Scheda_ElettricitaGas"`
Scheda_FonteEnergetica *string `xml:"Scheda_FonteEnergetica"`
Scheda_Giardino *string `xml:"Scheda_Giardino"`
Scheda_Grado *string `xml:"Scheda_Grado"`
Scheda_IncassoAnnuo *string `xml:"Scheda_IncassoAnnuo"`
Scheda_IndiceEdificabilita *string `xml:"Scheda_IndiceEdificabilita"`
Scheda_Infissi *string `xml:"Scheda_Infissi"`
Scheda_Internet *string `xml:"Scheda_Internet"`
Scheda_LatiLiberi *string `xml:"Scheda_LatiLiberi"`
Scheda_Magazzino *string `xml:"Scheda_Magazzino"`
Scheda_NumeroCamere *string `xml:"Scheda_NumeroCamere"`
Scheda_NumeroDipendenti *string `xml:"Scheda_NumeroDipendenti"`
Scheda_NumeroVetrine *string `xml:"Scheda_NumeroVetrine"`
Scheda_OneriUrbanizzazione *string `xml:"Scheda_OneriUrbanizzazione"`
Scheda_OrariApertura *string `xml:"Scheda_OrariApertura"`
Scheda_Orientamento *string `xml:"Scheda_Orientamento"`
Scheda_Panorama *string `xml:"Scheda_Panorama"`
Scheda_PavimentoBagno *string `xml:"Scheda_PavimentoBagno"`
Scheda_PavimentoCucina *string `xml:"Scheda_PavimentoCucina"`
Scheda_PavimentoGiorno *string `xml:"Scheda_PavimentoGiorno"`
Scheda_PavimentoNotte *string `xml:"Scheda_PavimentoNotte"`
Scheda_Pavimento *string `xml:"Scheda_Pavimento"`
Scheda_Persiana *string `xml:"Scheda_Persiana"`
Scheda_Piano *string `xml:"Scheda_Piano"`
Scheda_Posizione *string `xml:"Scheda_Posizione"`
Scheda_PossibilitàAcquistoMuri *string `xml:"Scheda_PossibilitàAcquistoMuri"`
Scheda_PostiSedere *string `xml:"Scheda_PostiSedere"`
Scheda_Pulizie *string `xml:"Scheda_Pulizie"`
Scheda_Raffrescamento *string `xml:"Scheda_Raffrescamento"`
Scheda_Riscaldamento *string `xml:"Scheda_Riscaldamento"`
Scheda_ScadenzaContratto *string `xml:"Scheda_ScadenzaContratto"`
Scheda_Serramenti *string `xml:"Scheda_Serramenti"`
Scheda_SpCondominialiAnno *string `xml:"Scheda_SpCondominialiAnno"`
Scheda_SpazioParcheggio *string `xml:"Scheda_SpazioParcheggio"`
Scheda_StatoImmobile *string `xml:"Scheda_StatoImmobile"`
Scheda_Taverna *string `xml:"Scheda_Taverna"`
Scheda_Terrazzi *string `xml:"Scheda_Terrazzi"`
Scheda_TipoContratto *string `xml:"Scheda_TipoContratto"`
Scheda_TipoCucina *string `xml:"Scheda_TipoCucina"`
Scheda_TipoGestione *string `xml:"Scheda_TipoGestione"`
Scheda_TipoImpianto *string `xml:"Scheda_TipoImpianto"`
Scheda_TipoRegistr *string `xml:"Scheda_TipoRegistr"`
Scheda_TipoRiscaldamento *string `xml:"Scheda_TipoRiscaldamento"`
Scheda_TipoSaracinesca *string `xml:"Scheda_TipoSaracinesca"`
Scheda_TipoSoggiorno *string `xml:"Scheda_TipoSoggiorno"`
Scheda_TotalePiani *string `xml:"Scheda_TotalePiani"`
Scheda_Tv *string `xml:"Scheda_Tv"`
Accessori *[]string `xml:"Accessori"`
Titolo *string `xml:"Titolo"`
TitoloEn *string `xml:"Titolo_En"`
Descrizione *string `xml:"Descrizione"`
DescrizioneEn *string `xml:"Descrizione_En"`
Foto *[]string `xml:"Foto"`
Consegna *string `xml:"Consegna"`
Clienti *struct {
Cliente *struct {
Id *string `xml:"Id"`
Cognome *string `xml:"Cognome"`
Nome *string `xml:"Nome"`
Tel1 *string `xml:"Tel1"`
Tel2 *string `xml:"Tel2"`
Tel3 *string `xml:"Tel3"`
Email1 *string `xml:"Email1"`
} `xml:"Cliente"`
} `xml:"Clienti"`
Cap *string `xml:"Cap"`
Video *[]string `xml:"Video"`
AMMedias *[]struct {
AMVideo *struct {
Url *string `xml:"Url"`
} `xml:"AMVideo"`
} `xml:"AMMedias"`
}
type AnnunciParsed struct {
Annuncio []AnnuncioParsed
}
type AnnuncioParsed struct {
Accessori *[]string
Anno *string
Numero_bagni *string
Numero_balconi *string
Numero_box *string
Numero_camere *string
Cap *string
Categorie *[]string
Civico *string
Civico_secondario *string
Classe *string
Codice string
Comune *string
Consegna *int
Creato_il *time.Time
Desc_it *string
Desc_en *string
Email *string
Homepage *bool
Indirizzo *string
Indirizzo_secondario *string
Lat *string
Lat_secondario *string
Lon *string
Lon_secondario *string
Modificato_il *time.Time
Mq *string
Piano_palazzo *string
Piano *string
Numero_postiauto *string
Prezzo int
Provincia *string
Regione *string
Caratteristiche *map[string]interface{}
Locatore *string
Numero *string
Idlocatore *string
Stato *string
Numero_terrazzi *string
Tipo *string
Titolo_it *string
Titolo_en *string
Unita_condominio *string
Numero_vani *string
Url_foto *[]string
Url_video *[]string
Web *bool
}
type AnnunciDB struct {
Id int
Codice string
Locatore *string
Numero *string
Idlocatore *string
Indirizzo *string
Civico *string
Comune *string
Cap *string
Provincia *string
Regione *string
Lat *string
Lon *string
Indirizzo_secondario *string
Civico_secondario *string
Lat_secondario *string
Lon_secondario *string
Tipo *string
Categorie *[]string
Prezzo int
Anno *string
Consegna *string
Classe *string
Mq *string
Piano *string
Piano_palazzo *int
Unita_condominio *int
Numero_vani *int
Numero_camere *int
Numero_bagni *int
Numero_balconi *int
Numero_terrazzi *int
Numero_box *int
Numero_postiauto *int
Accessori *[]string
Titolo_it *string
Desc_it *string
Titolo_en *string
Desc_en *string
Stato *string
Web *bool
Caratteristiche *map[string]interface{}
Url_immagini *[]string
Homepage *bool
Url_video *[]string
Email *string
Creato_il *time.Time
Modificato_il *time.Time
}
type FilebrowserData struct {
Data []File
Folder *string
}
type File struct {
Name string
Size int64
Mode os.FileMode
ModTime string
IsDir bool
}
type DefaultCaratteristiche struct {
Scheda []struct {
Id string `xml:"id"`
Tagxml string `xml:"tagxml"`
} `xml:"scheda"`
}
type Caratteristica struct {
Id string
Tagxml string
}
type ListaCaratteristiche struct {
Caratteristiche []Caratteristica
}
type DefaultCategories struct {
Cat []struct {
ID string `xml:"id"`
Nome string `xml:"nome"`
} `xml:"cat"`
}
type Categoria struct {
Id string
Nome string
}
type Storageindex struct {
Id string
Created_at time.Time
Filename string
Ext string
From_admin bool
Expires_at *time.Time
}
type MiogestImagesRef_Table struct {
Codice string
Foto *[]string
}

23
apps/backend/pg_dumper.sh Normal file
View file

@ -0,0 +1,23 @@
#!/bin/bash
# Set the necessary environment variables
export PGHOST="localhost"
export PGPORT="5432"
export PGDATABASE="postgres"
export PGUSER="postgres"
export PGPASSWORD="rootpost"
# Call pg_dump.exe to generate the SQL file
"C:/Program Files/PostgreSQL/16/bin/pg_dump.exe" -f "./db-docker/initial/1_init.sql" --schema-only
"C:/Program Files/PostgreSQL/16/bin/pg_dump.exe" -f "./db-docker/initial/2_data.sql" --data-only -t prezziario -t banners -t etichette -t flags -t banlist -t testi_e_stringhe -t temp_tokens
# Check if the pg_dump command was successful
if [ $? -eq 0 ]; then
echo "SQL file generated successfully."
else
echo "Failed to generate SQL file."
fi
# Wait for user keypress
read -n 1 -s -r -p "Press any key to exit..."

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,59 @@
{{define "backup-upload"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Backup Upload</title>
<style>
body {
background-color: #1f1f1f;
/* Dark background */
font-family: Arial, sans-serif;
color: #fff;
/* White text */
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
h1 {
text-align: center;
}
.subgroup {
display: flex;
flex-direction: column;
gap: 2rem;
background-color: #303030;
border-radius: 15px;
padding: 1rem;
}
a {
color: #97c1ff;
}
</style>
</head>
<body>
<div class="container">
<h1>Backup Upload</h1>
<div class="subgroup">
<form action="/upload-backup" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file">
<button type="submit">Upload</button>
</form>
</div>
</div>
</body>
</html>
{{end}}

View file

@ -0,0 +1,163 @@
{{define "main"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard</title>
<style>
body {
background-color: #1f1f1f;
/* Dark background */
font-family: Arial, sans-serif;
color: #fff;
/* White text */
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
h1 {
text-align: center;
}
.button-group {
text-align: center;
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 5rem;
}
.button {
display: inline-block;
padding: 10px 20px;
margin: 10px;
background-color: #007bff;
/* Blue button */
color: #fff;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #0056b3;
/* Darker blue on hover */
}
.element {
display: flex;
gap: 1rem;
}
.subgroup {
display: flex;
flex-direction: column;
gap: 2rem;
background-color: #303030;
border-radius: 15px;
padding: 1rem;
}
</style>
</head>
<body>
<div class="container">
<h1>Dashboard Api:</h1>
<div class="subgroup" style="gap:0.5rem;">
<h3>Enviroment Variables:</h3>
<div class="element">
<button onclick="toggleImageOption()" class="button">Toggle</button>
<h5>Image Processing: {{.imageOption}}</h5>
</div>
<h5>Concurrent Images: {{.concurrentImages}}</h5>
</div>
<div class="button-group">
<div class="subgroup">
<div class="element">
<a href="/initcwebp" class="button" style="background-color: orange;">Inizializza cwebp</a>
</div>
<div class="element">
<a href="/health" class="button" style="background-color: green;">Health DB</a>
</div>
</div>
<div class="subgroup">
<div class="element">
<a href="/upload-backup" class="button">Upload Bkp</a>
<h4>Carica il file di backup</h4>
</div>
<div class="element">
<a href="/bkp" class="button">Open Bkp</a>
<h4>Legge il file di backup</h4>
</div>
<div class="element">
<a href="/parsebkp" class="button">Parse Bkp</a>
<h4>Prende i dati dal file di backup e li eabora</h4>
</div>
<div class="element">
<a href="/setbkp" class="button">Set Bkp in db</a>
<h4>Aggiorna i dati nel database con i dati provenienti dal backup</h4>
</div>
</div>
<div class="subgroup">
<div class="element">
<a href="/miogest" class="button">Fetch MioGest</a>
<h4>Riceve dati dal xml giornaliero</h4>
</div>
<div class="element">
<a href="/miogest-sample" class="button">MioGest Sample</a>
<h4>Sample del file xml giornaliero</h4>
</div>
<div class="element">
<a href="/parse" class="button">Parse Update</a>
<h4>Prende i dati da miogest e li eabora, scarica e converte le foto</h4>
</div>
<div class="element">
<a href="/update" class="button">Set Update in db</a>
<h4>Aggiorna i dati nel database con i dati provenienti da miogest</h4>
</div>
</div>
<div class="subgroup">
<div class="element">
<a href="/cats" class="button">Sample Caratteristiche</a>
</div>
<div class="element">
<a href="/categ" class="button">Sample Categorie</a>
</div>
<div class="element">
<a href="/images/resetimages" class="button" style="background-color: red;">Reset Images</a>
</div>
</div>
</div>
</div>
</body>
<script>
function toggleImageOption() {
fetch("/image-option-toggle", {
method: "POST",
})
.then(response => response.text())
.then(data => {
console.log(data);
location.reload();
});
}
</script>
</html>
{{end}}

260
apps/backend/server.go Normal file
View file

@ -0,0 +1,260 @@
package main
import (
storagehandlers "backend/storage_handlers"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
"time"
"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
)
var (
imgbasepath string
imageOption bool
concurrentImages int = 1
storagepath string
Db *DbHandler
Miogest *MiogestHandler
ImageManager *ImageRefManager
Storage storagehandlers.StorageHandler
tempStorageLimit int = 5
)
func main() {
// SETUP WORK DIRECTORY
pwd, err := os.Getwd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// join paths indipedently of the OS
imgbasepath = filepath.Join(pwd, "images")
fmt.Println("Image base path: ", imgbasepath)
storagepath = filepath.Join(pwd, "storage")
// SETUP ENV VARIABLES
err = godotenv.Load(".env")
if err != nil {
fmt.Println("Error loading .env file")
}
if os.Getenv("IMAGEOPTION") != "" {
if os.Getenv("IMAGEOPTION") == "true" {
imageOption = true
} else {
imageOption = false
}
}
if os.Getenv("CONCURRENT_IMAGES") != "" {
concurrentImages, err = strconv.Atoi(os.Getenv("CONCURRENT_IMAGES"))
if err != nil {
fmt.Println("Error parsing CONCURRENT_IMAGES:", err)
}
}
// SETUP DATABASE HANDLER
Db = NewDbHandler()
if Db == nil {
log.Fatal("Error connecting to db")
}
defer Db.Dbpool.Close()
// SETUP MIOGEST HANDLER
Miogest = NewMiogestHandler()
if Miogest == nil {
log.Fatal("Error connecting to miogest")
}
// SETUP IMAGE MANAGER
ImageManager = NewImageRefManager()
if ImageManager == nil {
log.Fatal("Error creating image manager")
}
// SETUP STORAGE HANDLER
storagehandlers.Storagepath = storagepath
var strategy storagehandlers.StorageStrategy = storagehandlers.FS
STORAGE_STRATEGY := os.Getenv("STORAGE_STRATEGY")
if STORAGE_STRATEGY == "s3" || STORAGE_STRATEGY == "minio" {
MINIO_URL := os.Getenv("MINIO_URL")
MINIO_ROOT_USER := os.Getenv("MINIO_ROOT_USER")
MINIO_ROOT_PASSWORD := os.Getenv("MINIO_ROOT_PASSWORD")
if MINIO_URL == "" || MINIO_ROOT_USER == "" || MINIO_ROOT_PASSWORD == "" {
fmt.Println("Error: MINIO_ROOT_USER, MINIO_ROOT_PASSWORD and MINIO_URL must be set")
strategy = storagehandlers.FS
} else {
strategy = storagehandlers.S3
}
}
h, err := storagehandlers.NewStorageHandler(strategy)
if err != nil {
log.Fatal("Error creating storage handler: " + err.Error())
}
Storage = h
fmt.Printf("Storage Strategy: %s\n", strategy)
// SETUP ECHO
e := SetupRoutes()
// SETUP PORT
port := "1323"
if os.Getenv("ASPNETCORE_PORT") != "" {
port = os.Getenv("ASPNETCORE_PORT")
}
fmt.Println("Image Option: ", imageOption)
fmt.Println("Concurrent Images: ", concurrentImages)
fmt.Println("Server url: http://localhost:" + port)
Conversion("test.jpg")
// START ECHO
e.Logger.Info(e.Start(":" + port))
}
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data any, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func SetupRoutes() *echo.Echo {
funcs := map[string]any{
"contains": strings.Contains,
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
"getImage": func(txt string) string {
return txt[7:]
},
"isWebp": func(txt string) bool {
return strings.Contains(txt, "webp")
},
}
t := &Template{
templates: template.Must(template.New("").Funcs(funcs).ParseGlob("public/views/*.html")),
}
e := echo.New()
e.Renderer = t
// MISC
e.Static("/static", "public/static")
e.GET("/", func(c echo.Context) error {
return c.Render(http.StatusOK, "main", map[string]any{
"imageOption": imageOption,
"concurrentImages": concurrentImages,
})
})
e.GET("/test", func(c echo.Context) error {
return c.String(http.StatusOK, "OK")
})
e.POST("/testpost", func(c echo.Context) error {
// Get the form value
test := c.FormValue("test")
fmt.Println("test:", test)
return c.String(http.StatusOK, "OK")
})
e.GET("/health", func(c echo.Context) error {
_, err := Db.Dbpool.Exec(Db.Ctx, "SELECT 1")
if err != nil {
panic(err)
}
return c.String(http.StatusOK, "success")
})
MiogestRoutes(e)
BkpRoutes(e)
StorageRoutes(e)
ImageRoutes(e)
return e
}
func BkpRoutes(e *echo.Echo) {
e.GET("/upload-backup", func(c echo.Context) error {
return c.Render(http.StatusOK, "backup-upload", nil)
})
e.POST("/upload-backup", func(c echo.Context) error {
file, err := c.FormFile("file")
if err != nil {
return err
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
dst, err := os.Create("bkp.xml")
if err != nil {
return err
}
defer dst.Close()
if _, err = io.Copy(dst, src); err != nil {
return err
}
return c.HTML(http.StatusOK, fmt.Sprintf("<p>File %s uploaded successfully.</p>", file.Filename))
})
e.GET("/bkp", func(c echo.Context) error {
data := From_Backup()
return c.JSONPretty(http.StatusOK, data, " ")
})
e.GET("/parsebkp", func(c echo.Context) error {
var data = ParseUpdateBkp()
return c.JSONPretty(http.StatusOK, data, " ")
})
e.GET("/setbkp", func(c echo.Context) error {
var data = ParseUpdateBkp()
Db.SetFromBkp(data)
return c.String(http.StatusOK, "Setted")
})
}
func MiogestRoutes(e *echo.Echo) {
// MIOGEST DEFAULTS
e.GET("/cats", func(c echo.Context) error {
var data = Miogest.GetCaratteristiche()
return c.JSONPretty(http.StatusOK, data, " ")
})
e.GET("/categ", func(c echo.Context) error {
var data = Miogest.GetCategorie()
return c.JSONPretty(http.StatusOK, data, " ")
})
// MIOGEST UPDATE
e.GET("/miogest", func(c echo.Context) error {
data := Miogest.GetAnnunci()
return c.JSONPretty(http.StatusOK, data, " ")
})
e.GET("/miogest-sample", func(c echo.Context) error {
xml := Miogest.GetAnnunci()
rand := rand.Intn(len(xml.Annuncio))
data := xml.Annuncio[rand]
return c.JSONPretty(http.StatusOK, data, " ")
})
e.GET("/parse", func(c echo.Context) error {
start := time.Now()
var data = Miogest.GetAnnunciParsed()
end := time.Now()
fmt.Println("time taken:", end.Sub(start).String())
return c.JSONPretty(http.StatusOK, data, " ")
})
e.GET("/update", func(c echo.Context) error {
var data = Miogest.GetAnnunciParsed()
Db.InsertAnnunciInDB(data)
return c.String(http.StatusOK, "Updated")
})
}

263
apps/backend/storage.go Normal file
View file

@ -0,0 +1,263 @@
package main
import (
"errors"
"fmt"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"slices"
"time"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/labstack/echo/v4"
)
// Upload saves a file to the storage directory.
func Upload(file *multipart.FileHeader, from_admin bool) (string, error) {
log.Printf("Uploading file: %s", file.Filename)
log.Printf("DB instance: %v", Db.Dbpool)
var newId string
err := Db.Dbpool.QueryRow(Db.Ctx, "INSERT INTO public.storageindex(filename)VALUES (null) RETURNING id;").Scan(&newId)
if err != nil {
log.Fatal(err)
}
if newId == "" {
return "", errors.New("failed to create new id")
}
err = Storage.Add(file, newId)
if err != nil {
return "", err
}
//if !from_admin , expires in 60 days
var expiresAt *time.Time
if !from_admin {
exp := time.Now().AddDate(0, 2, 0) // 60 days
expiresAt = &exp
} else {
expiresAt = nil
}
_, err = Db.Dbpool.Exec(Db.Ctx, "UPDATE public.storageindex set filename= $2, ext = $3, from_admin = $4, expires_at = $5 where id = $1", newId, file.Filename, filepath.Ext(file.Filename), from_admin, expiresAt)
if err != nil {
log.Fatal(err)
}
_, err = Db.Dbpool.Exec(Db.Ctx, "DELETE FROM public.storageindex WHERE filename IS NULL")
if err != nil {
log.Fatal(err)
}
return newId, nil
}
func StorageAuth(c echo.Context) error {
token := c.Request().URL.Query().Get("token")
if len(token) < 1 {
return c.String(http.StatusUnauthorized, "Unauthorized")
}
var tk []string
err := pgxscan.Select(Db.Ctx, Db.Dbpool, &tk, "SELECT token FROM public.temp_tokens WHERE token = $1 and elapse > $2", token, time.Now())
if err != nil {
return err
}
if tk == nil {
return errors.New("invalid token")
}
_, err = Db.Dbpool.Exec(Db.Ctx, "DELETE FROM public.temp_tokens WHERE elapse < $1", time.Now())
if err != nil {
return err
}
return nil
}
func RemoveFromDb(ref string) error {
_, err := Db.Dbpool.Exec(Db.Ctx, "DELETE FROM public.storageindex WHERE id = $1", ref)
if err != nil {
return err
}
return nil
}
func RemoveExpired() error {
//get all expired files
var items_db []Storageindex
err := pgxscan.Select(Db.Ctx, Db.Dbpool, &items_db, "SELECT * FROM public.storageindex WHERE expires_at is not null and expires_at < NOW()")
if err != nil {
return fmt.Errorf("failed to get expired items: %w", err)
}
for _, item := range items_db {
formattedName := item.Id + item.Ext
err = Storage.Delete(formattedName)
if err != nil {
return fmt.Errorf("failed to delete file %s: %w", formattedName, err)
}
err = RemoveFromDb(item.Id)
if err != nil {
return fmt.Errorf("failed to remove item from db: %w", err)
}
}
return nil
}
func CheckConcruency() error {
var items_db []struct {
Id string
Ext string
}
err := pgxscan.Select(Db.Ctx, Db.Dbpool, &items_db, "SELECT id, ext FROM public.storageindex")
if err != nil {
return err
}
files, err := Storage.List()
if err != nil {
return err
}
if len(items_db) != len(files) {
var founds []string
for _, item := range items_db {
formattedName := item.Id + item.Ext
found := slices.Contains(files, formattedName)
if found {
founds = append(founds, formattedName)
} else {
_, err := Db.Dbpool.Exec(Db.Ctx, "DELETE FROM public.storageindex WHERE id = $1", item.Id)
if err != nil {
return err
}
}
}
for _, file := range files {
found := slices.Contains(founds, file)
if !found {
fmt.Printf("File %v not found in db\n", file)
//TODO decidere se eliminarlo
}
}
}
return nil
}
func TempFolderCleaner() error {
files, err := os.ReadDir(filepath.Join(storagepath, "/tmp/"))
if err != nil {
return err
}
fmt.Println("Temp files: ", len(files))
if len(files) >= tempStorageLimit {
var oldest_Time time.Time
var oldest_File string
for _, file := range files {
file_infos, err := file.Info()
if err != nil {
return err
}
if oldest_Time.IsZero() || file_infos.ModTime().Before(oldest_Time) {
oldest_Time = file_infos.ModTime()
oldest_File = file.Name()
}
}
err = os.Remove(filepath.Join(storagepath, "/tmp/", oldest_File))
if err != nil {
return err
}
}
return nil
}
// ROUTES
func StorageRoutes(e *echo.Echo) {
e.GET("/storage/list", func(c echo.Context) error {
filenames, err := Storage.List()
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, filenames)
})
e.GET("/storage/get/:filename", func(c echo.Context) error {
err := TempFolderCleaner()
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to clean temp folder: "+err.Error())
}
err = StorageAuth(c)
if err != nil {
return c.JSON(http.StatusUnauthorized, "Unauthorized: "+err.Error())
}
filename := c.Param("filename")
path, err := Storage.Get(filename)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to get file: "+err.Error())
}
return c.File(path)
})
e.POST("/storage/upload", func(c echo.Context) error {
fmt.Println("Uploading file")
err := StorageAuth(c)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to verify Auth: "+err.Error())
}
from_admin := c.QueryParam("from_admin") == "true"
file, err := c.FormFile("file")
if err != nil {
return err
}
fileId, err := Upload(file, from_admin)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to upload file: "+err.Error())
}
return c.String(http.StatusOK, fileId)
})
e.DELETE("/storage/delete/:filename", func(c echo.Context) error {
err := StorageAuth(c)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to verify Auth: "+err.Error())
}
filename := c.Param("filename")
err = Storage.Delete(filename)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to remove file / file not found: "+err.Error())
}
return c.String(http.StatusOK, "File removed")
})
e.GET("/storage/remove-expired", func(c echo.Context) error {
err := RemoveExpired()
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to remove files / files not found: "+err.Error())
}
return c.String(http.StatusOK, "File removed")
})
e.GET("/storage/test", func(c echo.Context) error {
err := CheckConcruency()
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to check concruency: "+err.Error())
}
return c.String(http.StatusOK, "OK")
})
}

View file

@ -0,0 +1,74 @@
package storagehandlers
import (
"io"
"log"
"mime/multipart"
"os"
"path/filepath"
)
type Fs_Handler struct {
}
func NewFsHandler() *Fs_Handler {
return &Fs_Handler{}
}
func (f *Fs_Handler) Init() error {
if _, err := os.Stat(Storagepath); os.IsNotExist(err) {
return err
}
return nil
}
func (f *Fs_Handler) Add(file *multipart.FileHeader, fileId string) error {
src, err := file.Open()
if err != nil {
log.Printf("Error opening file: %v\n", err)
return err
}
defer src.Close()
extension := filepath.Ext(file.Filename)
destPath := filepath.Join(Storagepath, fileId+extension)
dst, err := os.Create(destPath)
if err != nil {
log.Println("Error creating file")
return err
}
defer dst.Close()
if _, err = io.Copy(dst, src); err != nil {
log.Println("Error copying file")
return err
}
return nil
}
func (f *Fs_Handler) Delete(filename string) error {
filePath := filepath.Join(Storagepath, filename)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return err
}
return os.Remove(filePath)
}
func (f *Fs_Handler) Get(filename string) (string, error) {
path := filepath.Join(Storagepath, filename)
if _, err := os.Stat(path); os.IsNotExist(err) {
return "", err
}
return path, nil
}
func (f *Fs_Handler) List() ([]string, error) {
files, err := os.ReadDir(Storagepath)
if err != nil {
return nil, err
}
var filenames []string
for _, file := range files {
filenames = append(filenames, file.Name())
}
return filenames, nil
}

View file

@ -0,0 +1,137 @@
package storagehandlers
import (
"context"
"fmt"
"log"
"mime/multipart"
"os"
"path/filepath"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var (
useSSL = false
bucketName = "storage"
)
type S3_Handler struct {
minioClient *minio.Client
}
func (s *S3_Handler) Init() error {
found, err := s.minioClient.BucketExists(context.Background(), bucketName)
if err != nil {
log.Println(err)
return err
}
if !found {
err = s.minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{})
if err != nil {
log.Println(err)
return err
}
log.Println("Bucket created successfully")
}
log.Println("Bucket found")
return nil
}
func NewS3Handler() (*S3_Handler, error) {
MINIO_URL := os.Getenv("MINIO_URL")
MINIO_ROOT_USER := os.Getenv("MINIO_ROOT_USER")
MINIO_ROOT_PASSWORD := os.Getenv("MINIO_ROOT_PASSWORD")
minioClient, err := minio.New(MINIO_URL, &minio.Options{
Creds: credentials.NewStaticV4(MINIO_ROOT_USER, MINIO_ROOT_PASSWORD, ""),
Secure: useSSL,
})
if err != nil {
log.Fatalln(err)
}
return &S3_Handler{
minioClient,
}, nil
}
func (s *S3_Handler) Add(file *multipart.FileHeader, fileId string) error {
ctx := context.Background()
src, err := file.Open()
if err != nil {
log.Printf("Error opening file: %v\n", err)
return err
}
defer src.Close()
extension := filepath.Ext(file.Filename)
objectName := fileId + extension
info, err := s.minioClient.PutObject(ctx, bucketName, objectName, src, file.Size, minio.PutObjectOptions{ContentType: "application/octet-stream"})
if err != nil {
log.Fatalln(err)
return err
}
log.Printf("Successfully uploaded %s of size %d\n", objectName, info.Size)
return nil
}
func (s *S3_Handler) Delete(filename string) error {
opts := minio.RemoveObjectOptions{
GovernanceBypass: true,
}
err := s.minioClient.RemoveObject(context.Background(), bucketName, filename, opts)
if err != nil {
log.Fatalln(err)
return err
}
return nil
}
// func (s *S3_Handler) DeleteM(filename string) error {
// opts := minio.RemoveObjectOptions{
// GovernanceBypass: true,
// }
// err := s.minioClient.RemoveObjects(context.Background(), bucketName, filename, opts)
// if err != nil {
// log.Fatalln(err)
// return err
// }
// return nil
// }
func (s *S3_Handler) Get(filename string) (string, error) {
localPath := filepath.Join(Storagepath, "/tmp/", filename)
err := s.minioClient.FGetObject(context.Background(), bucketName, filename, localPath, minio.GetObjectOptions{})
if err != nil {
fmt.Println(err)
return "", fmt.Errorf("error downloading file %s: %w", filename, err)
}
log.Printf("Successfully downloaded %s\n", filename)
return localPath, nil
}
func (s *S3_Handler) List() ([]string, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
objectCh := s.minioClient.ListObjects(ctx, bucketName, minio.ListObjectsOptions{
Prefix: "",
Recursive: true,
})
var list []string
for object := range objectCh {
if object.Err != nil {
log.Println(object.Err)
return nil, object.Err
}
list = append(list, object.Key)
}
return list, nil
}

View file

@ -0,0 +1,53 @@
package storagehandlers
import (
"errors"
"mime/multipart"
)
var (
Storagepath string
)
type StorageHandler interface {
Init() error
Add(file *multipart.FileHeader, fileId string) error
Delete(filename string) error
Get(filename string) (string, error)
List() ([]string, error)
}
type StorageStrategy string
const (
FS StorageStrategy = "fs"
S3 StorageStrategy = "s3"
)
func NewStorageHandler(strategy StorageStrategy) (StorageHandler, error) {
if Storagepath == "" {
return nil, errors.New("storage path not set")
}
switch strategy {
case FS:
h := NewFsHandler()
err := h.Init()
if err != nil {
return nil, err
}
return h, nil
case S3:
h, err := NewS3Handler()
if err != nil {
return nil, err
}
err = h.Init()
if err != nil {
return nil, err
}
return h, nil
default:
return nil, errors.New("invalid storage strategy")
}
}

BIN
apps/backend/test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

122
apps/backend/utils.go Normal file
View file

@ -0,0 +1,122 @@
package main
import (
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
)
func MakeUppercase(s string) string {
if s != "" && reflect.TypeOf(s).Kind() == reflect.String && len(s) > 0 {
return strings.ToUpper(string(s[0])) + s[1:]
}
return ""
}
func CheckStringIfPresent(value *string) string {
if value != nil {
return strings.TrimSpace(*value)
}
return ""
}
func CheckStringIfPresentBool(value string) bool {
if value != "" {
return true
} else {
return false
}
}
func fetchXML(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return []byte{}, fmt.Errorf("GET error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return []byte{}, fmt.Errorf("status error: %v", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return []byte{}, fmt.Errorf("read body: %v", err)
}
return data, nil
}
func GetDataXML[T any](url string) T {
xmlBytes, err := fetchXML(url)
if err != nil {
log.Fatalf("Failed to get XML: %v", err)
panic(err)
}
var result T
err = xml.Unmarshal(xmlBytes, &result)
if err != nil {
log.Fatalf("Failed to unmarshal XML: %v", err)
panic(err)
}
return result
}
func GetPathsArray(path string) ([]string, error) {
var paths []string
err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
paths = append(paths, path)
}
return nil
})
if err != nil {
return nil, err
}
return paths, nil
}
func GetPathDirsArray(path string) []string {
var folderNames []string
files, err := os.ReadDir(path)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
if file.IsDir() {
folderNames = append(folderNames, file.Name())
}
}
return folderNames
}
func GetStringValue(strPtr *string) string {
if strPtr != nil {
return *strPtr
}
return ""
}
func RemoveWhitespace(str string) string {
return strings.ReplaceAll(str, " ", "")
}
func CommaToDot(str *string) *string {
if str != nil {
result := strings.ReplaceAll(*str, ",", ".")
return &result
}
return nil
}

19
apps/db/Dockerfile Normal file
View file

@ -0,0 +1,19 @@
# Start a new stage for PostgreSQL
FROM postgres:16 AS postgres
# Set environment variables for PostgreSQL
ARG POSTGRES_USER
ARG POSTGRES_PASSWORD
ARG POSTGRES_DB
ENV POSTGRES_USER=$POSTGRES_USER
ENV POSTGRES_PASSWORD=$POSTGRES_PASSWORD
ENV POSTGRES_DB=$POSTGRES_DB
# Copy the schema.sql file to the docker-entrypoint-initdb.d directory
COPY ./initial/*.sql /docker-entrypoint-initdb.d/
# Expose the PostgreSQL port
EXPOSE 5432
CMD ["-p", "5432"]

View file

@ -0,0 +1,34 @@
services:
db:
build:
context: .
dockerfile: Dockerfile
args:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
restart: always
networks:
- dokploy-network
expose:
- "5432"
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
interval: 1s
timeout: 5s
retries: 10
networks:
dokploy-network:
external: true
volumes:
dbdata:

72
apps/db/filter_sql.awk Normal file
View file

@ -0,0 +1,72 @@
# filter_sql.awk
BEGIN {
in_sql_block = 0;
sql_command = "";
sql_start_pattern = "ISTRUZIONE:|LOG: *istruzione:";
target_commands_regex = "(CREATE TABLE|ALTER TABLE|DROP TABLE)";
# If git_cutoff_date_str is empty (no Git repo or commit), set a dummy value to avoid error
# and set perform_cutoff to false. Otherwise, it's true.
perform_cutoff = (git_cutoff_date_str != "");
if (!perform_cutoff) {
git_cutoff_date_str = ""; # Ensure it's empty if not performing cutoff
}
current_log_timestamp_full = ""; # Initialize for each log file
}
# --- NEW AWK LOGIC FOR CUTOFF ---
# Rule 1: Always check for a new log entry header and extract its timestamp.
# This must be the first pattern rule in awk.
$0 ~ new_log_regex {
# Always update the timestamp if it's a new log line
current_log_timestamp_full = substr($0, 1, 23); # YYYY-MM-DD HH:MM:SS.ms (23 chars)
# Rule 1a: If a cutoff is active AND the current log entry is OLDER than the cutoff date.
# String comparison works for YYYY-MM-DD HH:MM:SS.ms format.
if (perform_cutoff && current_log_timestamp_full < git_cutoff_date_str) {
# If the current log entry is too old, discard any partially accumulated SQL command
# that might have started before this old log line was encountered.
in_sql_block = 0;
sql_command = "";
next; # Skip to the next line immediately.
}
# Rule 1b: If we were previously in an SQL block and this is a new, valid (not-cut-off) log entry,
# then the previous command is complete and should be processed.
if (in_sql_block) {
gsub(/^[ \t\n]+|[ \t\n]+$/, "", sql_command);
if (tolower(sql_command) ~ tolower(target_commands_regex)) {
print sql_command "\n";
}
sql_command = ""; # Reset for the next command
in_sql_block = 0; # Reset the flag
}
}
# Rule 2: If the line starts an SQL statement (and it passed the cutoff check or no cutoff is applied).
# This rule will only run for lines that were *not* skipped by Rule 1a.
$0 ~ sql_start_pattern {
line_content = $0;
sub(/.*(ISTRUZIONE:|LOG: *istruzione:)[ \t]*/, "", line_content);
sql_command = line_content;
in_sql_block = 1;
next; # Skip to the next line.
}
# Rule 3: If we are in an SQL block (meaning it's a continuation line for an SQL command).
# This rule will only run for lines that were *not* skipped by Rule 1a or handled by Rule 2.
in_sql_block {
sql_command = sql_command "\n" $0;
}
END {
# This block processes the very last command in the file if it was still accumulating
if (in_sql_block) {
gsub(/^[ \t\n]+|[ \t\n]+$/, "", sql_command);
if (tolower(sql_command) ~ tolower(target_commands_regex)) {
print sql_command "\n";
}
}
}

1135
apps/db/initial/1_init.sql Normal file

File diff suppressed because it is too large Load diff

133
apps/db/initial/2_data.sql Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
INSERT INTO
PUBLIC.USERS (
USERNAME,
EMAIL,
"isAdmin",
PASSWORD,
NOME,
COGNOME,
"isVerified",
SALT
)
VALUES (
'Marco Pedone'::TEXT,
'm.pedone98@gmail.com'::TEXT,
TRUE::BOOLEAN,
'changeme'::TEXT,
'Marco'::TEXT,
'Pedone'::TEXT,
TRUE::BOOLEAN,
'a'::TEXT
);

61
apps/db/log_parser.sh Normal file
View file

@ -0,0 +1,61 @@
#!/bin/bash
# Define the directory where PostgreSQL logs are stored
LOG_DIR="/c/Program Files/PostgreSQL/16/data/log"
# Define the output file for extracted SQL commands
OUTPUT_FILE="migration.sql"
# Define the AWK script file path
AWK_SCRIPT_FILE="filter_sql.awk" # Make sure this file is in the same directory as your bash script
# Clear the output file if it exists, or create a new one
> "$OUTPUT_FILE"
echo "Starting SQL command extraction from logs in: $LOG_DIR"
echo "Extracted commands will be saved to: $OUTPUT_FILE"
echo "----------------------------------------------------"
# --- NEW PART: GET LAST GIT COMMIT DATE ---
GIT_LAST_COMMIT_DATE_MS="" # This will store the Git commit date in YYYY-MM-DD HH:MM:SS.000 format
# Check if we are inside a Git repository
if git rev-parse --is-inside-work-tree &>/dev/null; then
# Get the last commit date in ISO 8601 strict format (e.g., 2025-07-31T16:21:44+02:00)
GIT_LAST_COMMIT_ISO=$(git log -1 --format=%cd --date=iso-strict)
if [ -n "$GIT_LAST_COMMIT_ISO" ]; then
# Convert to YYYY-MM-DD HH:MM:SS.000 for comparison with log timestamps
# cut -d'.' -f1 gets "YYYY-MM-DDTHH:MM:SS"
# tr 'T' ' ' changes 'T' to space
# "\.000" appends milliseconds for consistent string comparison
GIT_LAST_COMMIT_DATE_MS=$(echo "$GIT_LAST_COMMIT_ISO" | cut -d'.' -f1 | tr 'T' ' ')"\.000"
echo "Git last commit date detected (for log cutoff): $GIT_LAST_COMMIT_DATE_MS"
else
echo "Warning: Could not get last Git commit date. Processing all log entries."
fi
else
echo "Not inside a Git repository. Processing all log entries."
fi
# Regex to detect the start of a new log entry (used by awk)
NEW_LOG_ENTRY_REGEX="^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}"
# --- Check if the AWK script file exists ---
if [ ! -f "$AWK_SCRIPT_FILE" ]; then
echo "ERROR: AWK script file '$AWK_SCRIPT_FILE' not found!"
echo "Please create '$AWK_SCRIPT_FILE' with the AWK code from Step 1."
exit 1
fi
# Find all .log files in the specified directory and process them
find "$LOG_DIR" -type f -name "*.log" | while read -r logfile; do
echo "Processing log file: $logfile"
LC_ALL=C awk -v new_log_regex="$NEW_LOG_ENTRY_REGEX" \
-v git_cutoff_date_str="$GIT_LAST_COMMIT_DATE_MS" \
-f "$AWK_SCRIPT_FILE" "$logfile" >> "$OUTPUT_FILE"
done
echo "SQL extraction complete"
echo "----------------------------------------------------"
echo "Final SQL output is in: '$OUTPUT_FILE'."

0
apps/db/migration.sql Normal file
View file

23
apps/db/pg_dumper.sh Normal file
View file

@ -0,0 +1,23 @@
#!/bin/bash
# Set the necessary environment variables
export PGHOST="localhost"
export PGPORT="5432"
export PGDATABASE="postgres"
export PGUSER="postgres"
export PGPASSWORD="rootpost"
# Call pg_dump.exe to generate the SQL file
"C:/Program Files/PostgreSQL/16/bin/pg_dump.exe" -f "./initial/1_init.sql" --schema-only
"C:/Program Files/PostgreSQL/16/bin/pg_dump.exe" -f "./initial/2_data.sql" --data-only -t prezziario -t banners -t etichette -t flags -t banlist -t testi_e_stringhe -t temp_tokens
# Check if the pg_dump command was successful
if [ $? -eq 0 ]; then
echo "SQL file generated successfully."
else
echo "Failed to generate SQL file."
fi
# Wait for user keypress
read -n 1 -s -r -p "Press any key to exit..."

View file

@ -0,0 +1,25 @@
#Api key for the mail service
SENDGRID_API_KEY='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'
#Stripe keys
STRIPE_SECRET_KEY = "sk_test_51P4IY3ILe4KoQRqXJJLyn33Dvb4z3ajTrMsGD4y8g4DWXMbPQcoqOAcvedemB83xn0FIdVAMGX1hk8gXYog1BXl700cRUngUET"
NEXT_PUBLIC_STRIPE_PUBLIC_KEY = "pk_test_51P4IY3ILe4KoQRqXXUVDyiVEy9oiKdzDIB9xgDY054xVRyVZ5dfWoJlo1F5dXlQ6sEATwgylPp708PQEf2uhagUx00qcL0rRgq"
STRIPE_LIMITED_KEY = "rk_test_"
STRIPE_WEBHOOK_SECRET = "whsec_a1a62f5c31b2ade9237af6032e30392dda47f945e2eddd7e2f69579f1fb47118"
#Fatture in Cloud keys
FIC_CLIENT_ID = "ggZlcim78qkRYrl5n2d8DpNoKGiqwRHa"
FIC_ACCESS_TOKEN = "a/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyZWYiOiJ2Z2VOWFhST0JyZUliN3dNMFFpM0NpdXFhb1VHTzBlciJ9.ewEBe0Qq4ATNyx03rTTynWHhhxYvjuCB-ra8XBJV_B0"
FIC_COMPANY_ID = "120838"
#ARUBA EMAIL
ARUBA_USER = "web@infoalloggi.it"
ARUBA_PASS = "we2023Info.1"
#EXPOSED API AUTH
EXP_API_USER = "infoalloggi-admin"
EXP_API_PASS = "cyYk24u9fRn5DsqjFWpTV8GKBNwhQvEtbMmHxJeU73ZSaXrdCLAPg6NtDkC43ZQa9dVKHbxMv7fWpy65gFmAsEReXwuPYTL82jJnSUhrzcqGkqvsHPyxmEAFzYVSntGeQTr25dJb3UNCMuRpgwaDW69Zhc7Kj8LfB4ZCHR9kYfWVsGrx7tygqdvc82SFXj54Nhnpm6PEBLbKUuaMTQJeA3wDgPSCXwDVmhned6J8ZQ3qzNsr5bBuLtHxvFTAKGy74W2fY9pckjMRUEJYRkVPZwbvBtTKy2F8ULAseunzWj7G5d6Q3ExfmNCXqg4MDahH9cSruXyWxLNAsGQTPSFhD9HYVzB3Meg2rk5Cvacfq8mdZ6tUE4KRpwbn7jJAev7WRbVgPdncMLKxwm8yT9pQZ4DNUkaYj6ztXfhsBHqF5ErCu32Gcjb3H6KxvWsu9p5fkPrh7ZARXMLDdS82enFU4CqwztTayEmGNVgYJB"
JWT_SECRET = "335922ae76f8334dd11fa2614434624017ee21bffea95e355c0ac0a3b4e717193cdefbe4fadb5dac98939d6c4cd8b3158d530ebc8bb670281300efe5d5642fec"

View file

@ -0,0 +1,9 @@
TODO
node_modules
.vscode
.next
.git
Dockerfile
.dockerignore
.env
.docker_env

21
apps/infoalloggi/.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,21 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
#ignore:
# - dependency-name: "react"
# - dependency-name: "react-dom"
# - dependency-name: "@next/eslint-plugin-next"
# versions: [">=14.2.23"]
# - dependency-name: "next"
# versions: [">=14.2.23"]
# - dependency-name: "eslint-config-next"
# versions: [">=14.2.23"]
open-pull-requests-limit: 50

View file

@ -0,0 +1,56 @@
name: CI
on: [push, pull_request]
env:
INTERNAL_BASE_URL: "http://web:3000"
PGHOST: "localhost"
POSTGRES_USER: "test"
POSTGRES_DB: "test"
POSTGRES_PASSWORD: "test"
PGPORT: 123
BACKENDSERVER_URL: "https://fake.com"
NEXT_PUBLIC_BASE_URL: "https://fake.com"
STRIPE_SECRET_KEY: "string"
STRIPE_WEBHOOK_SECRET: "string"
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: "string"
STRIPE_LIMITED_KEY: "string"
FIC_CLIENT_ID: "string"
FIC_ACCESS_TOKEN: "string"
FIC_COMPANY_ID: "string"
NEXT_TELEMETRY_DISABLED: 1
ARUBA_USER: "string"
ARUBA_PASS: "string"
EXP_API_USER: "string"
EXP_API_PASS: "string"
TILES_URL: "keydb://string"
KEYDB_URL: "keydb://string"
JWT_SECRET: "335922ae"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install Dependencies
run: npm install
- name: Typecheck
run: npm run types
- name: Lint
run: npm run lint
- name: Print Environment Variable
run: echo $MY_ENV_VAR
#- name: Run knip
# run: npx knip --no-exit-code

View file

@ -0,0 +1,24 @@
const { makeKyselyHook } = require("kanel-kysely");
const ignoredTables = [];
/** @type {import('kanel').Config} */
module.exports = {
connection: {
host: "localhost",
user: "postgres",
database: "postgres",
password: "rootpost",
port: 5432,
},
typeFilter: (pgType) => !ignoredTables.includes(pgType.name),
preDeleteOutputFolder: true,
outputPath: "./src/schemas",
customTypeMap: {
"pg_catalog.tsvector": "string",
"pg_catalog.bpchar": "string",
//"pg_catalog.json": "string",
},
preRenderHooks: [makeKyselyHook()],
};

28
apps/infoalloggi/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
]
}

29
apps/infoalloggi/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,29 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib",
"sqltools.connections": [
{
"previewLimit": 50,
"server": "localhost",
"port": 5432,
"driver": "PostgreSQL",
"name": "DB",
"database": "postgres",
"username": "postgres",
"password": "rootpost"
}
],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

135
apps/infoalloggi/Dockerfile Normal file
View file

@ -0,0 +1,135 @@
FROM node:lts-alpine AS base
# Dependencies stage
# This stage is used to install dependencies and prepare the environment
FROM base AS deps
RUN apk add --no-cache libc6-compat openssl
WORKDIR /app
COPY package*.json package-lock.json* ./
RUN npm ci
# Builder stage
# This stage is used to build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG BASE_URL
ENV NEXT_PUBLIC_BASE_URL=$BASE_URL
ARG INTERNAL_BASE_URL
ENV INTERNAL_BASE_URL=$INTERNAL_BASE_URL
ARG POSTGRES_USER
ARG POSTGRES_PASSWORD
ARG POSTGRES_DB
ARG PGHOST
ARG PGPORT
ENV PGHOST=$PGHOST
ENV POSTGRES_DB=$POSTGRES_DB
ENV PGPORT=$PGPORT
ENV POSTGRES_USER=$POSTGRES_USER
ENV POSTGRES_PASSWORD=$POSTGRES_PASSWORD
ARG JWT_SECRET
ENV JWT_SECRET=$JWT_SECRET
ARG BACKENDSERVER_URL
ENV BACKENDSERVER_URL=$BACKENDSERVER_URL
ARG SENDGRID_API_KEY
ENV SENDGRID_API_KEY=$SENDGRID_API_KEY
ARG STRIPE_SECRET_KEY
ENV STRIPE_SECRET_KEY=$STRIPE_SECRET_KEY
ARG NEXT_PUBLIC_STRIPE_PUBLIC_KEY
ENV NEXT_PUBLIC_STRIPE_PUBLIC_KEY=$NEXT_PUBLIC_STRIPE_PUBLIC_KEY
ARG STRIPE_LIMITED_KEY
ENV STRIPE_LIMITED_KEY=$STRIPE_LIMITED_KEY
ARG STRIPE_WEBHOOK_SECRET
ENV STRIPE_WEBHOOK_SECRET=$STRIPE_WEBHOOK_SECRET
ARG FIC_CLIENT_ID
ENV FIC_CLIENT_ID=$FIC_CLIENT_ID
ARG FIC_ACCESS_TOKEN
ENV FIC_ACCESS_TOKEN=$FIC_ACCESS_TOKEN
ARG FIC_COMPANY_ID
ENV FIC_COMPANY_ID=$FIC_COMPANY_ID
ARG ARUBA_USER
ENV ARUBA_USER=$ARUBA_USER
ARG ARUBA_PASS
ENV ARUBA_PASS=$ARUBA_PASS
ARG EXP_API_USER
ENV EXP_API_USER=$EXP_API_USER
ARG EXP_API_PASS
ENV EXP_API_PASS=$EXP_API_PASS
ARG KEYDB_URL
ENV KEYDB_URL=$KEYDB_URL
ARG TILES_URL
ENV TILES_URL=$TILES_URL
ENV NEXT_TELEMETRY_DISABLED=1
RUN SKIP_ENV_VALIDATION=1 npm run build
# Runner stage
# This stage is used to run the built application
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ARG BASE_URL
ENV NEXT_PUBLIC_BASE_URL=$BASE_URL
ARG INTERNAL_BASE_URL
ENV INTERNAL_BASE_URL=$INTERNAL_BASE_URL
ARG POSTGRES_USER
ARG POSTGRES_PASSWORD
ARG POSTGRES_DB
ARG PGHOST
ARG PGPORT
ENV PGHOST=$PGHOST
ENV POSTGRES_DB=$POSTGRES_DB
ENV PGPORT=$PGPORT
ENV POSTGRES_USER=$POSTGRES_USER
ENV POSTGRES_PASSWORD=$POSTGRES_PASSWORD
ARG JWT_SECRET
ENV JWT_SECRET=$JWT_SECRET
ARG BACKENDSERVER_URL
ENV BACKENDSERVER_URL=$BACKENDSERVER_URL
ARG SENDGRID_API_KEY
ENV SENDGRID_API_KEY=$SENDGRID_API_KEY
ARG STRIPE_SECRET_KEY
ENV STRIPE_SECRET_KEY=$STRIPE_SECRET_KEY
ARG NEXT_PUBLIC_STRIPE_PUBLIC_KEY
ENV NEXT_PUBLIC_STRIPE_PUBLIC_KEY=$NEXT_PUBLIC_STRIPE_PUBLIC_KEY
ARG STRIPE_LIMITED_KEY
ENV STRIPE_LIMITED_KEY=$STRIPE_LIMITED_KEY
ARG STRIPE_WEBHOOK_SECRET
ENV STRIPE_WEBHOOK_SECRET=$STRIPE_WEBHOOK_SECRET
ARG FIC_CLIENT_ID
ENV FIC_CLIENT_ID=$FIC_CLIENT_ID
ARG FIC_ACCESS_TOKEN
ENV FIC_ACCESS_TOKEN=$FIC_ACCESS_TOKEN
ARG FIC_COMPANY_ID
ENV FIC_COMPANY_ID=$FIC_COMPANY_ID
ARG ARUBA_USER
ENV ARUBA_USER=$ARUBA_USER
ARG ARUBA_PASS
ENV ARUBA_PASS=$ARUBA_PASS
ARG EXP_API_USER
ENV EXP_API_USER=$EXP_API_USER
ARG EXP_API_PASS
ENV EXP_API_PASS=$EXP_API_PASS
ARG KEYDB_URL
ENV KEYDB_URL=$KEYDB_URL
ARG TILES_URL
ENV TILES_URL=$TILES_URL
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD HOSTNAME="0.0.0.0" node server.js

59
apps/infoalloggi/TODO Normal file
View file

@ -0,0 +1,59 @@
NEW IDEA TODOS:
//TODO predere privacy policy sito vecchio e adattarla
- trigger pag sucecss event from ordini tab and gen payment button in order detail
Minimal Viable Product:
- Msg and email queue. se è fuori orario il prop verrà notificato domani mattina
- tab locazioni
- GDPR Iubenda
- Translations
MVP Extras:
- admin annunci salvati: Da admin vedere indirizzo e interno nella visualizzazione annunci salvati di immobile
- Admin annuncio link: se admin, in annuncio, collegamento rapido per copiare risposta x email con dati di tipologia, persone e decorrenza annuncio
- info annunci/pack icon: Info Annunci/pack su navbar, icona con casetta con forse numeretti tipocontatore icone con dropdown con n annunci salvati n annunci sbloccati
- Extensions: integrare estensioni copiare dati x miogest e packs ecc
- Provincie options in maiuscolo
AFTER MVP:
- TODO migrazione app router https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration#migrating-from-pages-to-app
- Proprietari: Pagina proprietari e submit annuncio e Sezione "I miei annunci" con lista degli annunci inseriti dall'utente
- Sezione "La mia locazione" con i dati della locazione attuale dell'utente
- prenotazione visita, con data e ora, e conferma da parte del proprietario, con notifica a entrambi, tab con propietari, giorni e orari di disponibilità per annunci che offrono il servizio
- Tutorials
- lettura contatori
- Ricezione e Rilascio immobili
- consigli presa e rilascio appartamento
- Third party apps: Etra, add to calendario, scarica calendario pdf
- Report andamento props: Generate a Report with selected user review, analytics and other data for the apartment owner
- Infoalloggi.AI: Pagina area riservata "Infoalloggi.it AI" o "la mia ricerca Infoalloggi" robe cosí in cui il sistema fa una ricerca personalizzata, elemento marketizzabile
- Document scan: Document scan for upload
- indirizzo autocomplete nomatim:
```
https://github.com/tomickigrzegorz/autocomplete
function nominatim(currentValue) {
const api = `https://nominatim.openstreetmap.org/search?format=geojson&limit=5&q=${encodeURI(
currentValue
)}`;
return new Promise((resolve) => {
fetch(api)
.then((response) => response.json())
.then((data) => {
resolve(data.features);
})
.catch((error) => {
console.error(error);
});
});
}
```

View file

@ -0,0 +1,45 @@
/**
* @example
* ```ts
* interface BaseType = {
* a: string; b: number
* };
*
* interface BiggerType extends BaseType = {
* c: string[]
* }
* type Bar = BiggerType;
* // Bar = BiggerType
*
* type Bar = Prettify<BiggerType>;
* // Bar = { a: string; b: number; c: string[]; }
* ```
*/
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
//
export type PrettifyR<T> = T extends object
? T extends infer O
? { [K in keyof O]: PrettifyR<O[K]> }
: never
: T;
/**
* @example
* ```ts
* interface BaseType = {
* a: string; b: number
* };
*
* interface NewType = {
* a: number
* }
*
* type Bar = Override<BaseType, NewType>;
* // Bar = { a: number; b: number; }
*
* ```
* */
export type Override<Type, NewType> = Omit<Type, keyof NewType> & NewType;

View file

@ -0,0 +1,50 @@
const fs = require("node:fs");
const YAML = require("yaml");
const ciFilepath = ".github/workflows/ci.yml";
const envFilepath = "src/env.mjs";
const ignoreEnvs = ["NODE_ENV", "SKIP_ENV_VALIDATION"];
// ANSI escape codes for colors
const colors = {
red: "\x1b[31m%s\x1b[0m",
green: "\x1b[32m%s\x1b[0m",
yellow: "\x1b[33m%s\x1b[0m",
};
console.log();
console.log(colors.yellow, "Environment variables checker:");
// Read CI file content
try {
const ciContent = fs.readFileSync(ciFilepath, "utf8");
const ciData = YAML.parse(ciContent);
// Read env file content
const envContent = fs.readFileSync(envFilepath, "utf8");
const envRegex = /process\.env\.([A-Z_0-9]+)/g;
const declaredEnvs = [];
let match = envRegex.exec(envContent);
while (match !== null) {
declaredEnvs.push(match[1]);
match = envRegex.exec(envContent); // Re-assign at the end of the loop
}
// Remove ignored environment variables from the list of declared environment variables
const filteredEnvs = declaredEnvs.filter((env) => !ignoreEnvs.includes(env));
// Check for missing env variables in CI file
const missingEnvs = filteredEnvs.filter((env) => !ciData.env[env]);
if (missingEnvs.length > 0) {
console.error(
colors.red,
`Missing environment variables in ${ciFilepath}: ${missingEnvs.join(", ")}`,
);
} else {
console.log(colors.green, "No missing environment variables found.");
}
} catch (error) {
console.error(colors.red, `Error processing files: ${error.message}`);
}

View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "~/components",
"utils": "~/lib/utils",
"ui": "~/components/ui",
"lib": "~/lib",
"hooks": "~/hooks"
},
"iconLibrary": "lucide"
}

View file

@ -0,0 +1,7 @@
#!/bin/bash
# Start the pgAdmin service using its Docker Compose file
docker compose -p pgadmin -f pgadmin-docker-compose.yml up -d
# Start the KeyDB service using its Docker Compose file
docker compose -p keydb -f keydb-docker-compose.yml up -d

View file

@ -0,0 +1,63 @@
services:
web:
platform: "linux/amd64"
build:
context: .
dockerfile: Dockerfile
args:
INTERNAL_BASE_URL: http://web:3000
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGHOST: db
PGPORT: "5432"
BACKENDSERVER_URL: http://backend:1323
TILES_URL: tiles:6379
KEYDB_URL: keydb:6379
BASE_URL: ${BASE_URL}
SENDGRID_API_KEY: ${SENDGRID_API_KEY}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${NEXT_PUBLIC_STRIPE_PUBLIC_KEY}
STRIPE_LIMITED_KEY: ${STRIPE_LIMITED_KEY}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
FIC_CLIENT_ID: ${FIC_CLIENT_ID}
FIC_ACCESS_TOKEN: ${FIC_ACCESS_TOKEN}
FIC_COMPANY_ID: ${FIC_COMPANY_ID}
ARUBA_USER: ${ARUBA_USER}
ARUBA_PASS: ${ARUBA_PASS}
EXP_API_USER: ${EXP_API_USER}
EXP_API_PASS: ${EXP_API_PASS}
JWT_SECRET: ${JWT_SECRET}
working_dir: /app
environment:
INTERNAL_BASE_URL: http://web:3000
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGHOST: db
PGPORT: "5432"
BACKENDSERVER_URL: http://backend:1323
KEYDB_URL: keydb:6379
TILES_URL: tiles:6379
BASE_URL: ${BASE_URL}
SENDGRID_API_KEY: ${SENDGRID_API_KEY}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${NEXT_PUBLIC_STRIPE_PUBLIC_KEY}
STRIPE_LIMITED_KEY: ${STRIPE_LIMITED_KEY}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
FIC_CLIENT_ID: ${FIC_CLIENT_ID}
FIC_ACCESS_TOKEN: ${FIC_ACCESS_TOKEN}
FIC_COMPANY_ID: ${FIC_COMPANY_ID}
ARUBA_USER: ${ARUBA_USER}
ARUBA_PASS: ${ARUBA_PASS}
EXP_API_USER: ${EXP_API_USER}
EXP_API_PASS: ${EXP_API_PASS}
JWT_SECRET: ${JWT_SECRET}
networks:
- dokploy-network
ports:
- "3000"
networks:
dokploy-network:
external: true

View file

@ -0,0 +1,40 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type VerificaEmail_NewMail = {
mailType: "verificaEmail";
props: VerificaEmailProps;
};
type VerificaEmailProps = {
token: string;
userId: string;
redirect?: string;
};
const VerificaEmail = ({ token, userId, redirect }: VerificaEmailProps) => {
return (
<Base preview="Verifica la tua email" noreply>
<>
<Heading className="text-center text-3xl leading-8">
Verifica la tua email
</Heading>
<Section className="px-4 text-center text-base md:px-12">
<Row>
<Text>
Clicca sul pulsante qui sotto per verificare la tua email.
</Text>
</Row>
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/auth/verifica-email-action?t=${token}&u=${userId}${redirect ? `&r=${redirect}` : ""}`}
>
Clicca qui per verificare
</Button>
</Section>
</>
</Base>
);
};
export default VerificaEmail;

View file

@ -0,0 +1,81 @@
import {
Body,
Container,
Head,
Html,
Preview,
Section,
Text,
Tailwind,
Img,
Hr,
} from "@react-email/components";
import type { JSX } from "react";
const Base = ({
children,
preview,
noreply,
}: {
children: JSX.Element;
preview: string;
noreply?: boolean;
}) => {
return (
<Tailwind
config={{
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
},
}}
>
<Html>
<Head />
<Preview>{preview}</Preview>
<Body className="bg-neutral-50 py-2 text-center font-sans text-base text-neutral-700">
<Container className="max-w-xl bg-white pt-2 pb-10">
<Img
referrerPolicy="no-referrer"
width={230.5}
height={44.25}
src={
"https://lh3.google.com/u/0/d/1nEurjVWPiz0F8QRLhKcT4ZSujeFRChBs" //TEMP FIX
//getMediaUrl("/Infoalloggi.png")
}
alt="Infoalloggi.it"
className="mx-auto my-2 scale-80"
/>
<Section className="max-w-lg px-8">
<Hr />
<Section className="max-w-md">{children}</Section>
<Section className="mt-4">
{noreply && (
<Text className="text-xs text-neutral-500">
Questo è un messaggio automatico, per favore non rispondere
a questa email.
</Text>
)}
</Section>
<Section className="mt-3">
<Text className="text-left text-sm">
Arcenia S.r.l. Sede legale: Via Beata Giovanna 1, Bassano del
Grappa (VI)
</Text>
</Section>
</Section>
</Container>
</Body>
</Html>
</Tailwind>
);
};
export default Base;

View file

@ -0,0 +1,44 @@
import { Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type ContattoAdmin_NewMail = {
mailType: "contattoAdminEmail";
props: ContattoAdminEmailProps;
};
type ContattoAdminEmailProps = {
nome: string;
cognome: string;
email: string;
telefono: string;
messaggio: string;
};
const ContattoAdminEmail = ({
nome,
cognome,
email,
telefono,
messaggio,
}: ContattoAdminEmailProps) => {
return (
<Base preview="Contatto Email">
<>
<Heading className="text-center text-3xl leading-8">
Richiesta di contatto
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>Data: {new Date().toLocaleString()}</Text>
<Text>Nome: {nome}</Text>
<Text>Cognome: {cognome}</Text>
<Text>Email: {email}</Text>
<Text>Telefono: {telefono}</Text>
<Text>Messaggio: {messaggio}</Text>
</Row>
</Section>
</>
</Base>
);
};
export default ContattoAdminEmail;

View file

@ -0,0 +1,52 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type ContattoAnnuncio_NewMail = {
mailType: "contattoAnnuncioEmail";
props: ContattoAnnuncioEmailProps;
};
type ContattoAnnuncioEmailProps = {
nome: string;
codice: string;
email: string;
telefono: string;
messaggio: string;
};
const ContattoAnnuncioEmail = ({
nome,
codice,
email,
telefono,
messaggio,
}: ContattoAnnuncioEmailProps) => {
return (
<Base preview="Contatto Email">
<>
<Heading className="text-center text-3xl leading-8">
Richiesta di contatto
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>Data: {new Date().toLocaleString()}</Text>
<Text>Nome: {nome}</Text>
<Text>Annuncio: {codice}</Text>
<Text>Telefono: {telefono}</Text>
<Text>Email: {email}</Text>
<Text>Messaggio: {messaggio}</Text>
</Row>
</Section>
<Section className="mb-5">
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/annuncio/${codice}`}
>
Vai all&apos;annuncio
</Button>
</Section>
</>
</Base>
);
};
export default ContattoAnnuncioEmail;

View file

@ -0,0 +1,71 @@
import { Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type ContattoInteressato_NewMail = {
mailType: "emailContattoInteressato";
props: EmailContattoInteressatoProps;
};
type EmailContattoInteressatoProps = {
nome_cognome: string;
nome_cognome_utente: string;
telefono: string;
indirizzo: string | null;
sesso: string | null;
};
const EmailContattoInteressato = ({
nome_cognome,
nome_cognome_utente,
telefono,
indirizzo,
sesso,
}: EmailContattoInteressatoProps) => {
const sig = sesso
? sesso == "M"
? "Il Sig."
: "La Sig.ra"
: "Il/La Sig./Sig.ra";
return (
<Base preview="Contatto Email">
<>
<Heading className="text-center text-3xl leading-8">
Richiesta di contatto
</Heading>
<Section className="px-5 text-left md:px-12">
<Row>
<Text className="text-base">
Gentile proprietario {nome_cognome}, le comunichiamo che a breve
la contatterà {sig} {nome_cognome_utente} (tel: {telefono}),
cliente Arca abbonato tramite il nostro servizio online
Infoalloggi.it interessato a vedere il Suo immobile{" "}
{indirizzo ? "di " + indirizzo : ""}.
</Text>
<Text className="text-base">
Per ulteriori informazioni e chiarimenti non esiti a contattarci
all&apos;indirizzo{" "}
<a href="mailto:web@infoalloggi.it" rel="noopener noreferrer">
web@infoalloggi.it
</a>{" "}
oppure tramite tel. 04241760915.
</Text>
<Text className="text-base">Cordiali saluti</Text>
<br />
<Text className="text-xs">
{" "}
Le informazioni contenute in questo messaggio di posta elettronica
sono riservate confidenziali e ne è vietata la diffusione in
qualunque modo eseguita. Qualora Lei non fosse la persona a cui il
presente messaggio è destinato, La invitiamo gentilmente ad
eliminarlo dopo averne dato tempestiva comunicazione al mittente e
a non utilizzare in alcun caso il suo contenuto. Qualsiasi
utilizzo non autorizzato di questo messaggio e dei suoi eventuali
allegati espone il responsabile alle relative conseguenze civili e
penali.
</Text>
</Row>
</Section>
</>
</Base>
);
};
export default EmailContattoInteressato;

View file

@ -0,0 +1,48 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type Generic_NewMail = {
mailType: "generic";
props: GenericProps;
};
type GenericProps = {
title: string;
noreply?: boolean;
testo: string;
link?: {
href: string;
label?: string;
};
};
const Generic = ({
title,
noreply = false,
testo,
link = undefined,
}: GenericProps) => {
return (
<Base preview={title} noreply={noreply}>
<>
<Heading className="text-center text-3xl leading-8">{title}</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>{testo}</Text>
</Row>
{link && (
<Section className="mb-5">
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${link.href}`}
>
{link.label}
</Button>
</Section>
)}
</Section>
</>
</Base>
);
};
export default Generic;

View file

@ -0,0 +1,102 @@
import {
Button,
Heading,
Img,
Link,
Row,
Section,
Text,
} from "@react-email/components";
import Base from "./base";
export type OnboardingServizio_NewMail = {
mailType: "onboardingServizio";
props: OnboardingServizioProps;
};
type OnboardingServizioProps = {
token: string;
nome: string;
annunci?: {
titolo: string | null;
immagine: string | null;
codice: string;
prezzo: number;
}[];
};
const OnboardingServizio = ({
token,
nome,
annunci,
}: OnboardingServizioProps) => {
return (
<Base preview="Procedi ora su Infoalloggi.it">
<>
<Heading className="text-center text-3xl leading-8">
Procedi ora su Infoalloggi.it
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>
Ciao {nome},<br />
Accedi ora al servizio di ricerca Infoalloggi, dove potrai andare
a contattare direttamente i proprietari degli immobili
disponibili.
</Text>
</Row>
<Section className="mb-5">
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/servizio/pre-onboard/${token}`}
>
Vai al servizio
</Button>
</Section>
<Section>
{annunci?.map((annuncio, index) => (
<>
<Text className="font-semibold">
Annunci selezionati per te:
</Text>
<Section className="mb-[30px]" key={index}>
<Row className="mb-[12px]">
<Img
src={`${process.env.NEXT_PUBLIC_BASE_URL}/go-api/images/get/${annuncio.immagine}`}
width="100%"
height="140px"
alt={`Annuncio image - ${annuncio.codice}`}
className="block w-[240px] rounded-[4px] object-cover object-center"
/>
</Row>
<Row className="block w-full sm:hidden">
<div className="mb-[5px] flex h-[24px] w-fit items-center justify-center gap-1 rounded-full bg-indigo-600 px-2 text-[12px] leading-none font-semibold text-white">
<strong>Codice: </strong> {annuncio.codice}
</div>
<Heading
as="h2"
className="mt-0 mb-[6px] text-[16px] leading-none font-bold"
>
{annuncio.titolo || "N/A"}
</Heading>
<Text className="m-0 text-[14px] leading-[24px] text-gray-500">
<strong>Prezzo:</strong>
{(annuncio.prezzo / 1e2).toFixed(2)}
</Text>
<Link
href={`${process.env.NEXT_PUBLIC_BASE_URL}/annuncio/${annuncio.codice}`}
className="block text-[14px] font-semibold text-indigo-600 no-underline"
>
Scheda dell&apos;annuncio
</Link>
</Row>
</Section>
</>
))}
</Section>
</Section>
</>
</Base>
);
};
export default OnboardingServizio;

View file

@ -0,0 +1,42 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type PagamentoConferma_NewMail = {
mailType: "pagamentoConferma";
props: PagamentoConfermaProps;
};
type PagamentoConfermaProps = {
receiptHref: string;
};
const PagamentoConferma = ({ receiptHref }: PagamentoConfermaProps) => {
return (
<Base preview="Conferma Pagamento">
<>
<Heading className="text-center text-3xl leading-8">
Conferma Pagamento
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>
Gentile Cliente, la informiamo che il pagamento è stato confermato
con successo. Procederemo ad attivare i servizi acquistati se non
lo fossero già. Può controllare lo stato del suo account nella
sezione dedicata.
</Text>
</Row>
<Section className="mb-5">
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={receiptHref}
>
Vai al tuo account
</Button>
</Section>
</Section>
</>
</Base>
);
};
export default PagamentoConferma;

View file

@ -0,0 +1,36 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type PagamentoErrore_NewMail = {
mailType: "pagamentoErrore";
};
const PagamentoErrore = () => {
return (
<Base preview="Errore Pagamento">
<>
<Heading className="text-center text-3xl leading-8">
Errore Pagamento
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>
Gentile Cliente, la informiamo che il pagamento non è andato a
buon fine. Si prega di riprovare o contattare il nostro servizio
clienti per ulteriori informazioni.
</Text>
</Row>
<Section className="mb-5">
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard`}
>
Vai al tuo account
</Button>
</Section>
</Section>
</>
</Base>
);
};
export default PagamentoErrore;

View file

@ -0,0 +1,36 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type PagamentoRicezione_NewMail = {
mailType: "pagamentoRicezione";
};
const PagamentoRicezione = () => {
return (
<Base preview="Ricezione Pagamento">
<>
<Heading className="text-center text-3xl leading-8">
Ricezione Pagamento
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>
Gentile Cliente, le confermiamo la ricezione del pagamento. Le
invieremo una mail di conferma quando il pagamento sarà stato
processato.
</Text>
</Row>
<Section className="mb-5">
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard`}
>
Vai al tuo account
</Button>
</Section>
</Section>
</>
</Base>
);
};
export default PagamentoRicezione;

View file

@ -0,0 +1,39 @@
import { Heading, Row, Section, Text, Button } from "@react-email/components";
import Base from "./base";
export type PwResetLink_NewMail = {
mailType: "pwResetLink";
props: PwResetLinkProps;
};
type PwResetLinkProps = {
resetlink: string;
};
const PwResetLink = ({ resetlink }: PwResetLinkProps) => {
return (
<Base preview="Link Reset Password">
<>
<Heading className="text-center text-3xl leading-8">
Link Reset Password
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>
Abbiamo ricevuto la tua richiesta di reset password, clicca sul
link sottostante per procedere.
</Text>
</Row>
<Section className="mb-5">
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={resetlink}
>
Clicca qui per il reset della password
</Button>
</Section>
</Section>
</>
</Base>
);
};
export default PwResetLink;

View file

@ -0,0 +1,39 @@
import { Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type Recesso_NewMail = {
mailType: "recesso";
};
const Recesso = () => {
return (
<Base preview="Richiesta di recesso">
<>
<Heading className="text-center text-3xl leading-8">
Richiesta di recesso
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>
Gentile cliente, abbiamo ricevuto la tua richiesta di recesso.
Procederemo ad elaborare la richiesta e termineremo i servizi
attualemte attivi nel tuo account.
</Text>
<Text>
Per ulteriori informazioni e chiarimenti non esiti a contattarci
all&apos;indirizzo{" "}
<a
href="mailto:amministrazione@infoalloggi.it"
rel="noopener noreferrer"
>
amministrazione@infoalloggi.it{" "}
</a>
</Text>
<Text>Cordiali saluti</Text>
<br />
</Row>
</Section>
</>
</Base>
);
};
export default Recesso;

View file

@ -0,0 +1,35 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type RegistrazioneAvvenuta_NewMail = {
mailType: "registrazioneAvvenuta";
};
const RegistrazioneAvvenuta = () => {
return (
<Base preview="Registrazione Avvenuta">
<>
<Heading className="text-center text-3xl leading-8">
Registrazione avvenuta
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>
La tua registrazione è avvenuta con successo! Ora puoi accedere al
nostro sito con le tue credenziali.
</Text>
</Row>
<Section className="mb-5">
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard`}
>
Vai al tuo account
</Button>
</Section>
</Section>
</>
</Base>
);
};
export default RegistrazioneAvvenuta;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -0,0 +1,152 @@
import react from "eslint-plugin-react";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import pluginQuery from "@tanstack/eslint-plugin-query";
import kyselyRules from "./kyselyRules/plugin.js";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import noRelativeImportPaths from "eslint-plugin-no-relative-import-paths";
import { FlatCompat } from "@eslint/eslintrc";
import jsxA11y from "eslint-plugin-jsx-a11y";
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
...pluginQuery.configs["flat/recommended"],
{
ignores: [
"**/.next",
"**/out",
"**/public",
"**/node_modules/",
"**/check-env-vars.js",
"**/headers/",
"**/schemas/",
"**/kyselyRules/",
"prettier.config.cjs",
"postcss.config.cjs",
".kanelrc.js",
],
},
...compat.extends(
"eslint:recommended",
"plugin:react/recommended",
"plugin:@next/next/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"plugin:jsx-a11y/recommended",
),
{
plugins: {
react,
"@typescript-eslint": typescriptEslint,
"better-tailwindcss": eslintPluginBetterTailwindcss,
"jsx-a11y": jsxA11y,
kyselyRules: kyselyRules,
//"no-relative-import-paths": noRelativeImportPaths,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 5,
sourceType: "script",
parserOptions: {
project: true,
tsconfigRootDir: "./",
},
},
linterOptions: {
reportUnusedDisableDirectives: true,
},
settings: {
react: {
version: "detect",
},
"import/resolver": {
typescript: {
alwaysTryTypes: true,
project: "./tsconfig.json",
},
},
"better-tailwindcss": {
entryPoint: "src/styles/globals.css",
},
},
rules: {
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "off",
"react/prop-types": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/no-redundant-type-constituents": "warn",
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/prefer-optional-chain": "off",
"@typescript-eslint/prefer-nullish-coalescing": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/consistent-type-imports": [
"warn",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/switch-exhaustiveness-check": "warn",
"@typescript-eslint/no-misused-promises": [
"error",
{
checksVoidReturn: {
attributes: false,
},
},
],
"@next/next/no-img-element": "warn",
"kyselyRules/enforce-select": "error",
"kyselyRules/enforce-where": "error",
"kyselyRules/enforce-null": "error",
...eslintPluginBetterTailwindcss.configs["recommended-warn"]?.rules,
...eslintPluginBetterTailwindcss.configs["recommended-error"]?.rules,
"better-tailwindcss/enforce-consistent-line-wrapping": "off",
"better-tailwindcss/enforce-consistent-class-order": "off",
},
},
{
//ignores: ["**/emails/**/*.tsx", "next.config.js"],
files: ["src/**/*.tsx", "src/**/*.ts"],
plugins: {
"no-relative-import-paths": noRelativeImportPaths,
},
rules: {
"no-relative-import-paths/no-relative-import-paths": [
"warn",
{ rootDir: "src", prefix: "~" },
],
},
},
];

View file

@ -0,0 +1,13 @@
console.log(
"\x1b[34m%s\x1b[0m",
`
____ _______ __ _ _ _____ _____ ____ ____
| _ \\| ____\\ \\ / / | | | |_ _|_ _| _ \\/ ___|
| | | | _| \\ \\ / / | |_| | | | | | | |_) \\___ \\
| |_| | |___ \\ V / | _ | | | | | | __/ ___) |
|____/|_____| \\_/ |_| |_| |_| |_| |_| |____/
🩺 Running Development...
`,
);

View file

@ -0,0 +1,13 @@
console.log(
"\x1b[32m%s\x1b[0m",
`
____ _______ __
| _ \\| ____\\ \\ / /
| | | | _| \\ \\ / /
| |_| | |___ \\ V /
|____/|_____| \\_/
🩺 Running Development...
`,
);

View file

@ -0,0 +1,13 @@
console.log(
"\x1b[33m%s\x1b[0m",
`
_ _ _
| | (_) | |
| | _ _ __ | |_
| | | | '_ \\| __|
| |____| | | | | |_
\\_____/|_|_| |_|\\__|
🔍 Linting and Env Checks...
`,
);

View file

@ -0,0 +1,13 @@
console.log(
"\x1b[36m%s\x1b[0m",
`
____ ____ _____
| _ \\ / ___| |_ _| _ _ __ ___ __ _ ___ _ __
| |_) | | _ | || | | | \'_ \\ / _ \\/ _\` |/ _ \\ \'_ \\
| __/| |_| | | || |_| | |_) | __/ (_| | __/ | | |
|_| \\____| |_| \\__, | .__/ \\___|\\__, |\\___|_| |_|
|___/|_| |___/
📢 Importing Postgres schema...
`,
);

View file

@ -0,0 +1,13 @@
console.log(
"\x1b[31m%s\x1b[0m",
`
_____ ____ _ _
|_ _| _ _ __ ___ / ___| |__ ___ ___| | __
| || | | | \'_ \\ / _ \\ | | | \'_ \\ / _ \\/ __| |/ /
| || |_| | |_) | __/ | |___| | | | __/ (__| <
|_| \\__, | .__/ \\___| \\____|_| |_|\\___|\\___|_|\\_\\
|___/|_|
🌶 Typescript type check...
`,
);

View file

@ -0,0 +1,21 @@
services:
tiles:
image: eqalpha/keydb:latest
volumes:
- tiles-data:/data
ports:
- "6379:6379"
command: keydb-server /etc/keydb/keydb.conf --maxmemory 100mb --maxmemory-policy volatile-lru --maxmemory-samples 5
restart: unless-stopped
keydb:
image: eqalpha/keydb:latest
volumes:
- keydb-data:/data
ports:
- "6380:6379"
restart: unless-stopped
volumes:
tiles-data:
keydb-data:

View file

@ -0,0 +1,57 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"project": [
"src/**",
"**",
"!src/schemas/public/*.ts",
"!emails/**"
],
"ignore": [
".kanelrc.js",
".eslintrc.json",
"TypeHelpers.type.ts",
"src/utils/api.ts",
"src/server/api/trpc.ts",
"check-env-vars.js",
"src/components/ui/*",
"src/hooks/coordDistance.ts",
"src/hooks/useMobile.tsx",
"src/lib/nossr.tsx",
"src/lib/useIsTyping.ts",
"src/middlewares/apis_middleware.ts",
"src/server/auth.ts",
"src/utils/get-media-url.ts",
"src/utils/utils.ts"
],
"ignoreBinaries": [
"stripe"
],
"ignoreDependencies": [
"kysely-plugin-serialize",
"sharp",
"uuid",
"@react-email/components",
"@eslint/compat",
"@next/eslint-plugin-next",
"@prettier/sync",
"@types/eslint",
"@types/uuid",
"eslint-config-next",
"eslint-import-resolver-typescript",
"eslint-plugin-import",
"eslint-plugin-react-hooks",
"kanel-kysely",
"npm-run-all",
"typescript-eslint",
"@react-email/preview-server",
"@tailwindcss/forms",
"@tailwindcss/typography",
"tailwind-scrollbar",
"tw-animate-css",
"@radix-ui/react-dialog",
"@radix-ui/react-label",
"@radix-ui/react-slot",
"@radix-ui/react-tooltip",
"@radix-ui/react-popover"
]
}

View file

@ -0,0 +1,15 @@
const plugin = {
meta: {
name: "kyselyRules",
version: "1.0.0",
},
configs: {},
rules: {
"enforce-select": require("./rules/select"),
"enforce-where": require("./rules/where"),
"enforce-null": require("./rules/null"),
},
processors: {},
};
module.exports = plugin;

View file

@ -0,0 +1,107 @@
//@ts-nocheck
module.exports = {
meta: {
type: "problem",
docs: {
description:
"Ensure that all where condition that thest for null values utilize the correct operators.",
},
fixable: "code",
schema: [], // no options
},
create(context) {
const sourceCode = context.sourceCode;
const reportedNodes = new Set();
if (!/\.where\(/.test(sourceCode.text)) {
// Skip
return {};
}
return {
ExpressionStatement(node) {
evaluate(node.expression, sourceCode.getTokens(node));
},
AwaitExpression(node) {
evaluate(node.argument, sourceCode.getTokens(node));
},
};
function Check(expression, operator) {
const forbiddenOperators = [
"'='",
"'=='",
"'!='",
"'!=='",
'"="',
'"=="',
'"!="',
'"!=="',
];
if (forbiddenOperators.includes(operator.value)) {
const message = `The where condition is testing for null using the ${operator.value} operator. Ensure this is intentional and uses the correct operator.`;
context.report({
node: expression,
message,
fix: (fixer) => {
const correctOperator =
operator.value === "'='" ||
operator.value === "'=='" ||
operator.value === '"="' ||
operator.value === '"=="'
? "'is'"
: "'is not'";
return fixer.replaceText(operator, correctOperator);
},
});
}
}
function findNodes(expression, node, nodes) {
const nullNodes = nodes.filter((token) => token.value === "null");
if (nullNodes.length === 0) {
return;
}
for (let i = 0; i < nullNodes.length; i++) {
const nullNode = nullNodes[i];
// Avoid duplicate reports
if (reportedNodes.has(nullNode)) {
continue;
}
reportedNodes.add(nullNode);
const operator = sourceCode.getTokensBefore(nullNode, 2)[0];
if (!operator) {
return;
}
Check(expression, operator);
}
}
function evaluate(expression, nodes) {
if (!expression || expression.type !== "CallExpression") {
return true;
}
const whereNodes = nodes.filter((token) => token.value === "where");
if (whereNodes.length === 0 || !whereNodes) {
return;
}
if (whereNodes.length === 1) {
findNodes(expression, whereNodes[0], nodes);
return;
}
for (let i = 0; i < whereNodes.length; i++) {
const whereNode = whereNodes[i];
findNodes(expression, whereNode, nodes);
}
return;
}
},
};

View file

@ -0,0 +1,55 @@
const { RuleTester } = require("eslint");
const rule = require("./null");
const ruleTester = new RuleTester({
languageOptions: { ecmaVersion: 2015 },
});
ruleTester.run(
"enforce-null", // rule name
rule,
{
valid: [
{
code: "trx.selectFrom('name').select(['field']).where('something', 'is', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', 'is not', null).execute()",
},
],
invalid: [
{
code: "trx.selectFrom('name').select(['field']).where('something', '=', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', '==', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', '!=', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is not', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', '!==', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is not', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', 'is', null).where('something2', '=', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is', null).where('something2', 'is', null).execute()",
},
],
},
);
console.log("EnforceNull: All tests passed!");

View file

@ -0,0 +1,61 @@
//@ts-nocheck
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Description of the rule",
},
fixable: "code",
schema: [], // no options
},
create(context) {
const sourceCode = context.sourceCode;
if (!/\.selectFrom\(/.test(sourceCode.text)) {
// Skip
return {};
}
function evaluate(expression, nodes) {
if (!expression || expression.type !== "CallExpression") {
return true;
}
const hasSelectFrom = nodes.some((token) => token.value === "selectFrom");
if (hasSelectFrom) {
const hasSelect = nodes.some(
(token) => token.value === "select" || token.value === "selectAll",
);
if (!hasSelect) {
let closingParenthesisNode = null;
const selectNodeIndex = nodes.findIndex(
(token) => token.value === "selectFrom",
);
if (selectNodeIndex !== -1) {
closingParenthesisNode = nodes
.slice(selectNodeIndex)
.find((token) => token.value === ")");
}
context.report({
node: expression,
message: `Call chains containing "selectFrom" must also include a "select" or "selectAll" clause.`,
fix: (fixer) => {
return fixer.insertTextAfter(
closingParenthesisNode || nodes[6],
".selectAll()",
);
},
});
}
}
}
return {
ExpressionStatement(node) {
evaluate(node.expression, sourceCode.getTokens(node));
},
AwaitExpression(node) {
evaluate(node.argument, sourceCode.getTokens(node));
},
};
},
};

View file

@ -0,0 +1,31 @@
const { RuleTester } = require("eslint");
const rule = require("./select");
const ruleTester = new RuleTester({
languageOptions: { ecmaVersion: 2015 },
});
ruleTester.run(
"enforce-select", // rule name
rule,
{
valid: [
{
code: "trx.selectFrom('name').select(['field']).where('something', '=', something).execute()",
},
{
code: "trx.selectFrom('name').selectAll().where('something', '=', something).execute()",
},
],
invalid: [
{
code: "trx.selectFrom('name').where('something', '=', something).execute()",
errors: 1,
output:
"trx.selectFrom('name').selectAll().where('something', '=', something).execute()",
},
],
},
);
console.log("EnforceSelect: All tests passed!");

View file

@ -0,0 +1,52 @@
//@ts-nocheck
module.exports = {
meta: {
type: "problem",
docs: {
description:
"Ensures that any call chain which affects a table also includes a 'where' clause.",
},
fixable: "code",
schema: [], // no options
},
create(context) {
const sourceCode = context.sourceCode;
if (!/\.updateTable\(|\.deleteFrom\(/.test(sourceCode.text)) {
// Skip
return {};
}
return {
ExpressionStatement(node) {
evaluate(node.expression, sourceCode.getTokens(node));
},
AwaitExpression(node) {
evaluate(node.argument, sourceCode.getTokens(node));
},
};
function evaluate(expression, nodes) {
if (!expression || expression.type !== "CallExpression") {
return true;
}
const hasUpdateOrDelete = nodes.some(
(token) =>
token.value === "updateTable" || token.value === "deleteFrom",
);
if (hasUpdateOrDelete) {
const hasWhere = nodes.some((token) => token.value === "where");
if (!hasWhere) {
const message = `Call chains containing "updateTable" or "deleteFrom" must also include a "where" clause.`;
context.report({
node: expression,
message,
});
}
}
}
},
};

View file

@ -0,0 +1,26 @@
const { RuleTester } = require("eslint");
const rule = require("./where");
const ruleTester = new RuleTester({
languageOptions: { ecmaVersion: 2015 },
});
ruleTester.run(
"enforce-where", // rule name
rule,
{
valid: [
{
code: "trx.updateTable('name').set({foo:bar}).where('something', '=', something).execute()",
},
],
invalid: [
{
code: "trx.updateTable('name').set({foo:bar}).execute()",
errors: 1,
},
],
},
);
console.log("EnforceWhere: All tests passed!");

View file

@ -0,0 +1,88 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
* This is especially useful for Docker builds.
*/
import { env } from "node:process";
/** @type {import("next").NextConfig} */
const nextConfig = {
output: env.NODE_ENV === "production" ? "standalone" : undefined, // This is for docker builds
reactStrictMode: true,
i18n: {
locales: ["it", "en"],
defaultLocale: "it",
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "*.googleusercontent.com",
port: "",
pathname: "**",
},
{
hostname: env.BACKENDSERVER_URL || "localhost:1323",
},
],
minimumCacheTTL: 86400, // 1 day in seconds
},
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
transpilePackages: ["jotai-devtools"],
devIndicators: false,
rewrites: async () => {
return [
{
source: "/go-api/images/get/:slug*",
destination: env.BACKENDSERVER_URL + "/images/get/:slug*",
},
{
source: "/go-api/storage/get/:slug*",
destination: env.BACKENDSERVER_URL + "/storage/get/:slug*",
has: [
{ type: "cookie", key: "access_token" },
{
type: "query",
key: "token",
value:
"(?<token>^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$)",
},
],
},
{
source: "/go-api/storage/upload:slug*",
destination: env.BACKENDSERVER_URL + "/storage/upload:slug*",
has: [
{ type: "cookie", key: "access_token" },
{
type: "query",
key: "token",
value:
"(?<token>^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$)",
},
],
},
];
},
headers: async () => {
return [
{
source: "/:path*",
headers: [
{
key: "X-Frame-Options",
value: "",
},
],
},
];
},
};
export default nextConfig;

19903
apps/infoalloggi/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,146 @@
{
"name": "infoalloggi2",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "node ./headers/dev.js && next dev",
"dev-https": "node ./headers/dev-https.js && cross-env NEXT_PUBLIC_BASE_URL='https://localhost:3000' INTERNAL_BASE_URL='https://localhost:3000' next dev --experimental-https",
"idev": "cross-env NODE_OPTIONS='--inspect' next dev",
"lint": "node ./headers/lint.js && next lint && node check-env-vars.js",
"start": "next start",
"types": "node ./headers/type.js && tsc --noEmit",
"pg-typegen": "node ./headers/pg-typegen.js && npx kanel",
"email": "cross-env NEXT_PUBLIC_BASE_URL='https://localhost:3000' email dev --port 3500",
"test": "npm run lint && npm run types",
"knip": "knip --no-exit-code",
"stripe-dev": "stripe listen --forward-to http://localhost:3000/api/stripe-webhook",
"d": "npm run-p dev stripe-dev -cl",
"test-kr": "node ./kyselyRules/rules/select.test.js && node ./kyselyRules/rules/where.test.js&& node ./kyselyRules/rules/null.test.js"
},
"dependencies": {
"@fattureincloud/fattureincloud-ts-sdk": "^2.1.1",
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-toggle-group": "^1.1.10",
"@react-email/components": "^0.3.2",
"@react-email/render": "^1.1.3",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.5.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"@tiptap/extension-underline": "^2.23.0",
"@tiptap/pm": "^2.25.0",
"@tiptap/react": "^2.23.0",
"@tiptap/starter-kit": "^2.25.0",
"@trpc/client": "^11.1.0",
"@trpc/next": "^11.4.3",
"@trpc/react-query": "^11.1.0",
"@trpc/server": "^11.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"cookies-next": "^5.1.0",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.8.0",
"frimousse": "^0.3.0",
"ioredis": "^5.6.1",
"jose": "^6.0.12",
"js-cookie": "^3.0.5",
"kysely": "^0.28.2",
"kysely-plugin-serialize": "^0.8.2",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.2",
"lucide-react": "^0.525.0",
"next": "^15.3.4",
"next-themes": "^0.4.6",
"nodemailer": "^7.0.5",
"nodemailer-html-to-text": "^3.2.0",
"nuqs": "^2.4.3",
"pg": "^8.16.3",
"radix-ui": "^1.4.2",
"react": "^19.1.0",
"react-colorful": "^5.6.1",
"react-day-picker": "^9.8.0",
"react-dom": "^19.1.0",
"react-email": "^4.2.3",
"react-hook-form": "^7.60.0",
"react-hot-toast": "^2.5.2",
"react-is": "^19.1.0",
"react-leaflet": "^5.0.0",
"react-phone-number-input": "^3.4.12",
"react-reverse-portal": "^2.3.0",
"react-select": "^5.10.2",
"react-to-print": "^3.1.1",
"react-use": "^17.6.0",
"recharts": "^2.15.4",
"sharp": "^0.34.3",
"stripe": "^18.3.0",
"superjson": "2.2.2",
"tailwind-merge": "2.4",
"trpc-ui": "^1.0.15",
"type-fest": "^4.41.0",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"yaml": "^2.8.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/compat": "^1.3.1",
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.30.0",
"@hookform/devtools": "^4.4.0",
"@next/eslint-plugin-next": "^15.3.4",
"@prettier/sync": "^0.6.1",
"@react-email/preview-server": "^4.2.3",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.81.2",
"@total-typescript/ts-reset": "^0.6.1",
"@types/eslint": "^9.6.1",
"@types/eslint-plugin-jsx-a11y": "^6.10.0",
"@types/js-cookie": "^3.0.6",
"@types/leaflet": "^1.9.20",
"@types/node": "^24.0.7",
"@types/nodemailer": "^6.4.17",
"@types/nodemailer-html-to-text": "^3.1.3",
"@types/pg": "^8.15.4",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-is": "^19.0.0",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.31.0",
"cross-env": "^7.0.3",
"eslint": "^9.30.1",
"eslint-config-next": "^15.3.4",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-better-tailwindcss": "^3.5.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-no-relative-import-paths": "^1.6.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"jiti": "^2.4.2",
"kanel": "^3.14.1",
"kanel-kysely": "^0.7.1",
"knip": "^5.62.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.5.3",
"prettier": "^3.6.2",
"prettier-plugin-classnames": "^0.8.1",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.5",
"typescript": "^5.8.3",
"typescript-eslint": "^8.35.0"
},
"ct3aMetadata": {
"initVersion": "7.9.0"
}
}

View file

@ -0,0 +1,15 @@
services:
pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: m.pedone98@gmail.com
PGADMIN_DEFAULT_PASSWORD: rootpost
ports:
- "5050:80"
volumes:
- pgadmin_data:/var/lib/pgadmin
restart: unless-stopped
volumes:
pgadmin_data:

View file

@ -0,0 +1,9 @@
/* eslint-env node */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
module.exports = config;

View file

@ -0,0 +1,10 @@
/* eslint-env node */
/** @type {import("prettier").Config & import('prettier-plugin-tailwindcss').PluginOptions} */
const config = {
printWidth: 80,
plugins: ["prettier-plugin-classnames", "prettier-plugin-tailwindcss"],
customAttributes: ["className"],
customFunctions: ["classNames"],
};
module.exports = config;

View file

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 491.85 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>.cls-1{fill:#b1b4c4;}.cls-2{fill:#e4e7f2;}.cls-15,.cls-3,.cls-6{fill:#d6d8e5;}.cls-4{fill:#fff;}.cls-5{opacity:0.5;fill:url(#Безымянный_градиент_15);}.cls-6{opacity:0.4;}.cls-7{fill:#2b303f;}.cls-8{fill:#c3c6d1;}.cls-9{fill:#e9eaf2;}.cls-10{fill:#8c50ff;}.cls-11{fill:#f5f6ff;}.cls-12,.cls-13{fill:none;stroke:#e9eaf2;stroke-miterlimit:10;}.cls-12{stroke-width:0.22px;}.cls-13{stroke-width:1.09px;}.cls-14{fill:#fafbff;}.cls-15{opacity:0.3;}.cls-16,.cls-30{opacity:0.1;}.cls-17{opacity:0.15;}.cls-18{fill:#2b2a30;}.cls-19{fill:#ffcea9;}.cls-20{fill:#f4f4f4;}.cls-21{fill:#bf4b4b;}.cls-22{fill:#e26161;}.cls-23{fill:#ededed;}.cls-24{fill:#3a2c6d;}.cls-25{fill:#bfbfbf;}.cls-26{fill:#e5e5e5;}.cls-27{fill:#38226d;}.cls-28{fill:#9c73ff;}.cls-29{fill:#a1a5b5;}.cls-30{fill:#3e7eff;}</style>
</defs>
<title>Business_SVG</title>
<g transform="matrix(1, 0, 0, 1, 0, -62.849998)">
<path d="M 240.441 116.863 L 103.331 253.961 L 103.331 425.15 L 388.519 425.15 L 388.519 264.93 L 240.441 116.863 Z" fill="#EAEAEA"/>
<path d="M 158.175 304.495 L 234.956 304.495 L 234.956 425.15 L 158.175 425.15 L 158.175 304.495 Z" fill="#434854"/>
<path d="M 256.894 293.525 L 289.8 293.525 L 289.8 326.432 L 256.894 326.432 L 256.894 293.525 Z M 300.769 293.525 L 333.675 293.525 L 333.675 326.432 L 300.769 326.432 L 300.769 293.525 Z M 300.769 249.65 L 333.675 249.65 L 333.675 282.557 L 300.769 282.557 L 300.769 249.65 Z M 256.894 249.65 L 289.8 249.65 L 289.8 282.557 L 256.894 282.557 L 256.894 249.65 Z" fill="#469FCC"/>
<path d="M 256.894 249.65 L 289.8 249.65 L 289.8 260.62 L 256.894 260.62 L 256.894 249.65 Z M 300.769 249.65 L 333.675 249.65 L 333.675 260.62 L 300.769 260.62 L 300.769 249.65 Z M 256.894 293.525 L 289.8 293.525 L 289.8 304.495 L 256.894 304.495 L 256.894 293.525 Z M 300.769 293.525 L 333.675 293.525 L 333.675 304.495 L 300.769 304.495 L 300.769 293.525 Z" fill=""/>
<path d="M 240.441 96.088 L 103.331 238.682 L 103.331 271.588 L 240.441 134.479 L 388.519 282.557 L 388.519 249.65 L 240.441 96.088 Z" fill=""/>
<path d="M 417.509 247.38 L 255.95 85.822 L 248.195 78.066 C 243.915 73.778 236.967 73.778 232.686 78.066 L 224.931 85.822 L 74.341 236.411 C 70.053 240.692 70.053 247.641 74.341 251.921 L 82.096 259.676 C 86.377 263.964 93.325 263.964 97.606 259.676 L 240.441 116.863 L 394.234 270.656 C 398.514 274.944 405.462 274.944 409.743 270.656 L 417.498 262.901 C 421.784 258.617 421.79 251.67 417.509 247.38 Z" fill="#EF4D4D"/>
<path d="M 152.088 425.15 C 156.032 418.51 158.134 410.937 158.175 403.213 C 158.175 369.438 121.612 348.329 92.362 365.216 C 78.788 373.054 70.425 387.538 70.425 403.213 C 70.425 411.242 72.74 418.668 76.512 425.15 L 152.088 425.15 Z M 377.55 359.338 C 360.375 359.357 344.797 369.415 337.711 385.06 C 333.087 382.609 327.94 381.311 322.706 381.275 C 304.533 381.275 289.8 396.008 289.8 414.182 C 289.8 418.054 290.59 421.706 291.818 425.15 L 415.348 425.15 C 419.285 418.507 421.382 410.936 421.425 403.213 C 421.425 378.982 401.781 359.338 377.55 359.338 Z" fill="#3AAD73"/>
<path d="M 337.711 385.06 C 333.087 382.609 327.94 381.311 322.706 381.275 C 304.533 381.275 289.8 396.008 289.8 414.182 C 289.8 418.054 290.59 421.706 291.818 425.15 L 339.762 425.15 C 335.818 418.51 333.716 410.937 333.675 403.213 C 333.675 396.719 335.178 390.61 337.711 385.06 Z" fill=""/>
</g>
<g id="Girl_8" transform="matrix(1, 0, 0, 1, -9.090909, -35.950413)">
<ellipse class="cls-6" cx="455.85" cy="360.19" rx="22.69" ry="13.1"/>
<path id="_Контур_27" data-name="&lt;Контур&gt;" class="cls-24" d="M454.85,237.69c.91-1.46,7.86-2.42,10.15,3.47.86,2.23.73,7.94,1.46,11.52s2.43,5.62,3,8.53-.38,7.78-7.39,8.65-12.4-1.6-14.06-3.84-1.69-7,0-9.53,3-4.63,3-8Z"/>
<path class="cls-19" d="M446.53,262.33S444.88,274,448,282.21s2.25-2.13,2.25-2.13l-.75-2.46s-.62-9.5-.36-11.83.2-4.33.36-4.52-.61-.56-.94-.61S446.53,262.33,446.53,262.33Z"/>
<path id="_Контур_28" data-name="&lt;Контур&gt;" class="cls-24" d="M459.44,236.68s-5.23-1.51-8.4,1.32a7.77,7.77,0,0,0-2.24,7.75c.42,1.65,1.27,3.91,2.58,4S459.44,236.68,459.44,236.68Z"/>
<path id="_Контур_29" data-name="&lt;Контур&gt;" class="cls-19" d="M461.44,251s-.34,5.83-.23,6.26,2.28.84,2.9,1.35-3.17,6.42-5.1,7.3-7.88-1.36-8.29-4.33c-.38-2.72,1.7-4.84,2.51-5.58a12.41,12.41,0,0,1,1.64,0l.17-2.82C457.65,252.25,461.44,251,461.44,251Z"/>
<path id="_Контур_30" data-name="&lt;Контур&gt;" class="cls-20" d="M462.24,257.77c1.64.26,3.51,1.13,3.77,2a19.33,19.33,0,0,1-.47,9.73c-1.42,3.75-2.37,6.14-3.19,7.76,0,0-7.09,3-12.8-.51,0,0-.4-4-.42-6.17-5.28-4.6-.77-10.28,3.7-14.69a10.85,10.85,0,0,1,1.61,0s-4,4.72-1.21,7.4C459.16,261.5,460.34,258.84,462.24,257.77Z"/>
<path id="_Контур_31" data-name="&lt;Контур&gt;" class="cls-19" d="M450.55,243.72h0c.59-3.75,3.29-6.58,7.16-6.35A7.43,7.43,0,0,1,461.44,251a7.54,7.54,0,0,1-.59,1.63c-1.37,1.6-5,2.61-6.21,2.62-1,0-1.93-.95-2.9-2.56C449.71,249.29,450.23,245.57,450.55,243.72Z"/>
<path class="cls-24" d="M458.9,237.44c2.71.53,4.94,1.29,5.68,3.95.5,1.84,1.16,5.33.53,6.8l-.38,1.07L461.44,251s-1.53-1.28-1.05-5a2.8,2.8,0,0,0-.46-1.42,4.12,4.12,0,0,1-.46-1.13,4.25,4.25,0,0,0-2.12-2.3c-2.45-1.37-6-.19-6.52.31A7.11,7.11,0,0,1,458.9,237.44Z"/>
<path id="_Контур_32" data-name="&lt;Контур&gt;" class="cls-19" d="M466.2,285c.61-3.8,1.75-6.26,1.22-8.35a75.85,75.85,0,0,0-4.62-12.3c-1.2-2.36-.83-4,.41-4.94,1.4-1,2.73-.59,4,2,1.59,3.29,3.37,6.67,4.24,11.57a25.78,25.78,0,0,1-.12,9.57c-.54,3.41-1.62,5.33-2.91,8.53C468.06,292,465.68,288.24,466.2,285Z"/>
<path class="cls-27" d="M463.83,355.83c-1.37,1.11-3.51.34-3.61.44a55.48,55.48,0,0,1-5,3.84c-.86.67-2.19,1.57-1.95,2.87.43,2.31,4.13,1.6,5.53.89s2.53-1.88,3.85-2.7c.95-.59,1.87-.89,2-2.08C464.74,358.37,464.07,355.8,463.83,355.83Z"/>
<path class="cls-28" d="M464.63,358.61c-.18,1.14-1.09,1.46-2,2-1.34.83-2.52,2-3.92,2.74s-4.51,1.3-5.41-.35c.49,2.23,4.13,1.53,5.51.83s2.53-1.88,3.85-2.7c.95-.59,1.87-.89,2-2.08A2.88,2.88,0,0,0,464.63,358.61Z"/>
<path class="cls-27" d="M454.2,353.9c-1.25,1-3.15.45-3.24.55a52.4,52.4,0,0,1-4.59,3.5c-.79.61-2,1.44-1.79,2.62.4,2.12,3.79,1.47,5.07.82s2.32-1.72,3.53-2.47c.86-.54,1.71-.82,1.85-1.91C455.11,356.35,454.43,353.88,454.2,353.9Z"/>
<path class="cls-28" d="M455,356.57c-.17,1.05-1,1.33-1.86,1.87-1.23.76-2.31,1.86-3.59,2.51s-4.13,1.19-5-.32c.45,2,3.78,1.4,5,.76s2.32-1.72,3.53-2.47c.86-.54,1.71-.82,1.85-1.91A2.41,2.41,0,0,0,455,356.57Z"/>
<path class="cls-19" d="M446.81,290.1c.61-4.73,2.35-11.6,2.35-11.6a14,14,0,0,0,5.25,1.31,43.54,43.54,0,0,0,7.93-.78,47.67,47.67,0,0,0,2.8,4.33c1.36,1.89,4,6,3.37,13.29-.41,4.73-3.07,24.84-3.07,24.84a37.47,37.47,0,0,1,1.63,10.43,84,84,0,0,1-1.66,12.49l-1.58,11.42s-2.11,1.52-3.61.44l.19-11.17c-.18-2.73-.45-6.76-.63-9.34-.28-4.19-1-10.85-1.25-12.52s-.75-4.57-1-7.83-1.48-19.8-1.48-19.8l-.17,3.1s-.17,5-.89,11.24-1,9-1,9a13.34,13.34,0,0,1,1.42,4c.13,1.34,1.66,12,.74,17.36L454.29,354a3.61,3.61,0,0,1-3.25.43l-1.11-13.84c-.74-5.27-2.31-16.43-2.55-17.88a59.48,59.48,0,0,1-.94-8.85C446.07,307.87,446.21,294.83,446.81,290.1Z"/>
<path class="cls-10" d="M462.35,277.29l4.07,8.18s6.22,9.1-.22,32.48l1.21,7.21s-12.19,7.83-20.35,0c0,0-2.83-21.61-1.32-31.57a113.26,113.26,0,0,1,3.8-16.93s2,2.21,12.27.75"/>
<path class="cls-23" d="M469.66,266.05s-2.83,2.82-6.19,2.47L461.86,263s-1-2.87,1.4-4.13S468.25,260.43,469.66,266.05Z"/>
<path class="cls-23" d="M452.83,255.92s-3.24-1-6.67,6.19l1.28.77A54.66,54.66,0,0,1,452.83,255.92Z"/>
</g>
<g id="Girl_4" transform="matrix(1, 0, 0, 1, 40.495865, -45.041321)">
<ellipse class="cls-6" cx="85.39" cy="486.9" rx="22.69" ry="13.1"/>
<path id="_Контур_33" data-name="&lt;Контур&gt;" class="cls-19" d="M98.48,386.23c2,.57,3.12,2.76,4.86,7.45a39.25,39.25,0,0,1,2.09,13.22c-.56,4.42-2.39,7.86-6.81,11.44l-2.11-3.87s4.33-3.82,4.77-8.24c.25-2.48-2.66-10.84-2.66-11.07S98.48,386.23,98.48,386.23Z"/>
<path class="cls-27" d="M81.17,488.09a4.85,4.85,0,0,1-4-.54c-1.08,0-7.43-2.49-8.23-.38-.7,1.85,1.85,3.57,3.2,3.9,3,.71,5.16,2.19,6.36,2.44a3,3,0,0,0,2.74-.38C82.07,492.29,82,489.19,81.17,488.09Z"/>
<path class="cls-28" d="M78.48,493c-1.2-.25-3.39-1.73-6.36-2.45-1.13-.27-3.1-1.52-3.29-3-.27,1.7,2,3.21,3.29,3.52,3,.71,5.16,2.19,6.36,2.44a3,3,0,0,0,2.74-.38,1.91,1.91,0,0,0,.39-.71A3.56,3.56,0,0,1,78.48,493Z"/>
<path class="cls-27" d="M97.29,479.71a3.81,3.81,0,0,1-3.49-.53c-1,0-7.53-2.38-8.3-.35-.67,1.78,1.78,3.42,3.08,3.74,2.86.69,5,2.12,6.12,2.36a2.9,2.9,0,0,0,2.64-.37C98.16,483.75,98.09,480.77,97.29,479.71Z"/>
<path class="cls-28" d="M94.7,484.44c-1.15-.25-3.26-1.67-6.12-2.36-1.09-.27-3-1.47-3.17-2.9-.25,1.64,2,3.1,3.17,3.39,2.86.69,5,2.12,6.12,2.36a2.9,2.9,0,0,0,2.64-.37,1.67,1.67,0,0,0,.37-.69A3.4,3.4,0,0,1,94.7,484.44Z"/>
<path id="_Контур_34" data-name="&lt;Контур&gt;" class="cls-19" d="M99.42,463.6a21.09,21.09,0,0,0-1.24-7.15s.82-9.79,1.45-16.22c.92-9.48,2.38-10.42,2-16.18-.38-5.34-3.42-8.8-4.21-12.42l-15.47,1.22s-1.35,4.39-3.09,13.13S77,445.11,77.14,458.87c0,3.55-.6,6.63-.4,15.47.14,5.94.48,13.22.48,13.22,2.26,1.94,3.83.51,3.83.51s4-17.6,4.55-21.64a12.73,12.73,0,0,0-.54-6.68s1.16-5.53,2-9.88c1-5.31,3.65-16.77,3.65-16.77s-.45,17.61.32,21.08.9,6.49,1.56,11.21c.79,5.7,1.15,14.05,1.15,14.05,2.36,1.5,3.41.35,3.41.35S99.42,468.59,99.42,463.6Z"/>
<path class="cls-10" d="M97.38,411.63c1.62,1.66,4.3,6.71,4.3,14.35,0,8.6-1.25,6.86-1.85,16.33l-.6,9.47s-12.93,6.17-22.15.45c-.82-1.79.31-21.71,1.93-28.79s2.73-10.55,2.73-10.55S93.63,416.1,97.38,411.63Z"/>
<path id="_Контур_35" data-name="&lt;Контур&gt;" class="cls-19" d="M97.57,386.14c-3.3-.2-3.62.35-3.88-.82-.09-.42-.28-3.32-.28-3.32a10.61,10.61,0,0,0,.83-1.22,8.37,8.37,0,0,0,4-6.29,8.47,8.47,0,0,0-7.54-9.3c-4.37-.45-7.59,2.62-8.45,6.84-.46,2.08-.94,6.33.36,9.74.73,1.93,1.41,3.28,3.15,3.21a10.47,10.47,0,0,0,1.72-.3,16.18,16.18,0,0,1,.1,2.41c0,.85.1,1.27-1.56,2.29s2.5,3.1,5.21,2.9,5.75-1.84,6.59-3.59C98.74,386.73,99.12,386.23,97.57,386.14Z"/>
<path class="cls-23" d="M100,400.75s3.73-.84,5.44-2.5c0,0-2.22-7-3.64-8.74S99,399,99,399Z"/>
<path id="_Контур_36" data-name="&lt;Контур&gt;" class="cls-20" d="M94.29,386.07c.61.76.68,1.71-1.67,2.31a7.44,7.44,0,0,1-5.15-.4,34.52,34.52,0,0,0-5.32,2.82c-2,6.6-.39,7.45-1.23,12.18.33,2.38.72,8.52.82,9.91,4.49,3.14,13.6,2.07,15.64-1.26a27.55,27.55,0,0,1,.31-3.31c2.14-7.94,3.81-10,3.22-16-.34-3.42-.61-5.14-2.43-6.06A16.1,16.1,0,0,0,94.29,386.07Z"/>
<path id="_Контур_37" data-name="&lt;Контур&gt;" class="cls-19" d="M53.07,411.89a23.07,23.07,0,0,1,3,.86,10,10,0,0,0,2.93,1c1.1.17,2.82-.4,5.79-1.8a32,32,0,0,0,8.7-6.54,75.44,75.44,0,0,0,7.6-10.85c1.29-2.35,2.92-2.94,4.34-2.48,1.7.56,1.91,2.25,0,5.41a60.5,60.5,0,0,1-8.7,11.39,32.63,32.63,0,0,1-9.07,6.4,27.87,27.87,0,0,1-7.63,2.65c-.93.2-1.56.59-3.28,1.19a11.36,11.36,0,0,1-5.35.59c-1.49-.29-2-.63-2.06-1s.24-.63.94-.58a9.18,9.18,0,0,0,3.32-.08s-1.54-.05-2.45-.16a8.86,8.86,0,0,1-2.38-.6c-.73-.31-1-1.46-.26-1.42s1.24.26,2.6.4a9,9,0,0,0,2.16,0,16.91,16.91,0,0,1-2.65-.66,3.76,3.76,0,0,1-1.85-1.14c-.25-.29-.18-1,.69-.82a13.89,13.89,0,0,0,3.24.71c1.11,0,1.9-.09,1.76-.26s-.8-.15-1.64-.62-1.34-1.38-1-1.82S52.16,411.55,53.07,411.89Z"/>
<path class="cls-23" d="M83.64,401.31a12,12,0,0,1-6.84-3.47l4.14-5.74s2.33-2.94,5.41-1.28c0,0,2.32.25,0,5.11A47.84,47.84,0,0,1,83.64,401.31Z"/>
<path class="cls-24" d="M81.45,369.54a3.83,3.83,0,0,1,4.32-3.16,7.31,7.31,0,0,1,4.92-1.19c3.93.41,6.5,3,7.69,6.9a30.94,30.94,0,0,1,1.16,6c1.18,8.75,4.36,15.69,1.39,18.29S89.82,400.63,86.42,399s2.8-16.71-3.93-25.09C80.91,371.92,80.76,370.53,81.45,369.54Z"/>
</g>
<g id="Men_13" transform="matrix(1, 0, 0, 1, 40.495865, -45.041321)">
<ellipse class="cls-6" cx="22.69" cy="454.91" rx="22.69" ry="13.1"/>
<path class="cls-18" d="M12,451.62c1.55,1.26,3.83.11,3.94.23a72.22,72.22,0,0,0,5.71,4.75c1,.76,2.47,1.77,2.19,3.23-.48,2.6-4.65,1.8-6.23,1s-2.86-2.11-4.35-3c-1.06-.66-2.1-1-2.27-2.34C10.85,454.64,11.69,451.59,12,451.62Z"/>
<path class="cls-21" d="M11,454.91c.21,1.28,1.24,1.64,2.29,2.29,1.52.94,2.85,2.3,4.42,3.09s5.08,1.47,6.1-.39c-.55,2.51-4.65,1.72-6.21.93s-2.86-2.11-4.35-3c-1.06-.66-2.1-1-2.27-2.34A3.6,3.6,0,0,1,11,454.91Z"/>
<path class="cls-18" d="M25.38,446.63c1.55,1.26,4.25-.11,4.37,0a60,60,0,0,0,5.09,4.61c1,.75,2.47,1.77,2.2,3.23-.49,2.6-4.66,1.8-6.23,1s-2.86-2.12-4.35-3c-1.06-.66-2.11-1-2.27-2.34C24.09,449.28,25.11,446.6,25.38,446.63Z"/>
<path class="cls-21" d="M24.23,449.73c.21,1.28,1.24,1.64,2.29,2.29,1.52.94,2.85,2.3,4.42,3.1s5.08,1.46,6.1-.4c-.55,2.51-4.65,1.73-6.21.94s-2.85-2.12-4.34-3.05c-1.07-.66-2.11-1-2.28-2.34A3.6,3.6,0,0,1,24.23,449.73Z"/>
<path id="_Контур_38" data-name="&lt;Контур&gt;" class="cls-22" d="M11.87,394.54c.08,2.34.8,12.5,1.13,17.87s.4,12,.4,12-.93,3.91-1.53,8.52,0,19.13,0,19.13a3.67,3.67,0,0,0,4,0s2.48-11.63,3.62-16.87,1.35-8.26,1.94-11.5c.72-3.93,2.27-22.26,2.27-22.26l.62,0,1.66,20.69a23.16,23.16,0,0,0-1.05,4.7c-.45,3.37.48,20.33.48,20.33a4.1,4.1,0,0,0,4.28-.18s4.13-22.46,4.13-25.87c0-2.27-.38-32.07-.38-32.07Z"/>
<path id="_Контур_39" data-name="&lt;Контур&gt;" class="cls-19" d="M17.48,349.35s.26,5.1.15,5.55-2.34,1.94-3,2.48S18.16,362,20.19,363s8.46-1.52,8.57-4.68-1.1-5.81-2.08-5.95S17.48,349.35,17.48,349.35Z"/>
<path class="cls-19" d="M31.44,358.81c.3-2.25-1.18-4.39.25-6.18,5,4.94,4.4,18,5.54,26.35,3.74,3.9,11.25,7.75,13.36,8.7.38.18.72.37.64.78a9.26,9.26,0,0,1-1.75,3.82c-5-2.14-8.55-3.37-14.76-9.27a13.59,13.59,0,0,1-1.57-2.07C31.38,375.62,30.93,362.69,31.44,358.81Z"/>
<path id="_Контур_40" data-name="&lt;Контур&gt;" class="cls-20" d="M16.91,355.78s3.7,5.51,8.86,6c1.63-1.6,1.77-7.31-.48-9.14,0,0,2.07-1.67,3.49-1.35A7.35,7.35,0,0,1,32,355.39a33.52,33.52,0,0,1,1.82,11.8c-.09,5.41,0,21.94,0,21.94s-.72,3.49-5.39,6.09-8.14,2.86-11.31,2.12c-2.61-.61-4.77-1.53-5.67-3.42.24-3.8,1.93-13.69,1.3-18.46s-1.64-8.32-1.9-12.18.5-3.78,2.44-5.15A29.62,29.62,0,0,1,16.91,355.78Z"/>
<path id="_Контур_41" data-name="&lt;Контур&gt;" class="cls-19" d="M29,341.75h0c-.61-4-3.46-7-7.55-6.73a7.85,7.85,0,0,0-4,14.33,7.82,7.82,0,0,0,.63,1.73c1.44,1.69,5.81,2.73,7.06,2.55A3.74,3.74,0,0,0,28.42,351C29.78,347.86,29.35,343.7,29,341.75Z"/>
<path class="cls-18" d="M17.48,349.35h0l.37-3.46S15,338.59,21.7,340c4.4.93,5.5,1.33,6.84-1.62s-3.38-5.61-9-4.87a7.75,7.75,0,0,0-6.94,7.86C12.76,343.53,13.34,347.93,17.48,349.35Z"/>
<path class="cls-19" d="M9.67,361c1.93-.55.46-.1,1.93-.55,5,4.93,4.41,13.39,5.55,21.74C19.93,386.8,30.24,393,30.24,393c.13.89-.18,1.15-1.3,3a44.43,44.43,0,0,1-15.33-9.89c-.25-.23-.61-.3-.71-.61C11.13,380.14,9.16,364.89,9.67,361Z"/>
<polygon class="cls-20" points="55.55 371.31 48.06 396.93 31.61 403.5 37.61 378.54 55.55 371.31"/>
<path class="cls-19" d="M27.65,393.21c.24-.88.87-.66,1.59-.44a4.13,4.13,0,0,0,2.43.21,7.52,7.52,0,0,0,1.74-1.18,2.15,2.15,0,0,1,1.1-.31c0,.22-.38,1.52-.38,1.52s3.61,0,4,0a.61.61,0,0,1,.57,1l.25,0c.73-.16,1,1,.31,1.14l-.6.09c.48.19.6,1,0,1.1l-.19,0a.78.78,0,0,1-.26,0H38a.63.63,0,0,1-.54.76c-1.76.23-4.35.93-5.94-.52a9.88,9.88,0,0,1-3.19-.86C27.38,395.24,27.35,394.25,27.65,393.21Z"/>
<path class="cls-19" d="M45.63,391.11l.25,0a.6.6,0,0,1,.35-1.09c.37,0,4.08-.78,4.08-.78l.45-1.52c0-.09.64.3.66.32a2.46,2.46,0,0,1,1.12,1.85,4,4,0,0,1-1.07,2.87A3.66,3.66,0,0,1,49,393.82c-.45,0-.88.05-1.27.09a.65.65,0,0,1-.69-.64l-.12,0a.47.47,0,0,1-.26,0h-.19c-.63,0-.68-.78-.25-1.06l-.61,0C44.81,392.28,44.88,391.1,45.63,391.11Z"/>
<path class="cls-23" d="M33.54,362.15a4,4,0,0,0,2.68-1.2c-.14-1.9-2.56-9.09-7.44-9.63a9.09,9.09,0,0,1,3,3.68A48.52,48.52,0,0,1,33.54,362.15Z"/>
<path class="cls-23" d="M8.79,371.65s4.85,1.68,8.27-1.14c.38-.64-2-8.9-2-8.9s-2-4.22-5.22-2.54S8.79,371.65,8.79,371.65Z"/>
</g>
<g transform="matrix(1, 0, 0, 1, -94.214874, 11.983471)">
<path class="cls-1" d="M 425.06 388.31 C 423.503 386.888 421.792 385.644 419.96 384.6 C 406.11 376.6 383.59 376.6 369.75 384.6 C 367.612 385.809 365.647 387.303 363.91 389.04 C 356.3 396.85 358.25 406.95 369.75 413.59 C 383.59 421.59 406.12 421.59 419.96 413.59 C 431.82 406.74 433.52 396.21 425.06 388.31 Z M 415.55 411 C 404.14 417.59 385.55 417.59 374.17 411 C 366.17 406.38 363.77 399.72 367 393.88 C 368.739 391.021 371.216 388.682 374.17 387.11 C 385.58 380.53 404.17 380.53 415.55 387.11 C 418.501 388.679 420.972 391.019 422.7 393.88 C 425.94 399.76 423.55 406.41 415.55 411 Z"/>
<path class="cls-29" d="M 422.7 393.92 C 420.972 391.059 418.501 388.719 415.55 387.15 C 404.14 380.57 385.55 380.57 374.17 387.15 C 371.216 388.722 368.739 391.061 367 393.92 L 363.91 389 L 363.35 388.11 L 367.35 378.56 L 381.24 370.84 L 407.1 371.29 L 424.67 381.71 C 424.67 381.71 424.99 385.01 425.03 388.27 C 425.1 392.35 424.7 396.32 422.7 393.92 Z"/>
<polygon class="cls-1" points="359.37 399 359.38 388.15 364.76 397.26 359.37 399"/>
<polygon class="cls-1" points="430.34 399 430.34 388.21 425.6 393.58 430.34 399"/>
<ellipse class="cls-30" cx="395.18" cy="393.58" rx="29.54" ry="17.05"/>
<path class="cls-1" d="M 362.07 400.83 C 362.07 400.83 366.62 418.75 400.55 417.83 C 424.1 417.83 427.11 397.59 426.55 394.71 C 421.66 401.449 414.045 405.687 405.74 406.29 C 391.41 407.54 380.83 407.38 375.17 404.64 C 369.51 401.9 362.08 394.71 362.08 394.71 L 362.07 400.83 Z"/>
<path class="cls-3" d="M 369.75 403.25 C 355.91 395.25 355.91 382.25 369.75 374.25 C 383.59 366.25 406.12 366.25 419.96 374.25 C 433.8 382.25 433.8 395.25 419.96 403.25 C 406.12 411.25 383.59 411.24 369.75 403.25 Z M 415.54 376.81 C 404.13 370.23 385.54 370.22 374.17 376.81 C 362.8 383.4 362.76 394.11 374.17 400.7 C 385.58 407.29 404.17 407.29 415.54 400.7 C 426.91 394.11 427 383.4 415.54 376.81 Z"/>
<ellipse class="cls-16" cx="319.57" cy="438.88" rx="3.9" ry="6.75" transform="matrix(0.866025, -0.5, 0.5, 0.866025, -176.630124, 218.599962)"/>
<polygon class="cls-1" points="373.39 407.78 358.45 416.41 353.84 413.74 368.78 405.12 373.39 407.78"/>
<polygon class="cls-29" points="373.39 413.1 358.45 421.72 358.45 416.4 373.39 407.77 373.39 413.1"/>
<path class="cls-10" d="M 361.5 420.18 C 361.318 416.821 359.546 413.75 356.73 411.91 C 355.762 411.21 354.496 411.073 353.4 411.55 L 316.22 433 L 323 444.64 L 359.72 423.44 C 360.81 423.05 361.5 421.91 361.5 420.18 Z"/>
<ellipse class="cls-10" cx="319.57" cy="438.88" rx="3.9" ry="6.75" transform="matrix(0.866025, -0.5, 0.5, 0.866025, -176.630124, 218.599962)"/>
</g>
<g id="Men_1" transform="matrix(1, 0, 0, 1, -22.314049, -27.685949)">
<ellipse class="cls-6" cx="423.93" cy="486.82" rx="22.69" ry="13.1"/>
<path id="_Контур_54" data-name="&lt;Контур&gt;" class="cls-19" d="M439.85,392.73c.24,2.11,2.76,11.21,2.22,14.32s-9.14,8.33-9.14,8.33l-1.19-5.33,4.76-4.67-1.29-8.83Z"/>
<path id="_Контур_55" data-name="&lt;Контур&gt;" class="cls-23" d="M433.2,383.76a3.77,3.77,0,0,1,4.79,1.45c1.26,2.09,2.57,9.28,2.69,10.5,0,0-1.77,2.46-4.43,2.05Z"/>
<path class="cls-18" d="M423.38,485.22a7.4,7.4,0,0,1-5.56-.68c-1.34,0-8.62-3.07-9.61-.46-.86,2.3,2.29,4.42,4,4.82,3.68.89,6.39,2.73,7.87,3,1.1.23,2.52.38,3.4-.48C424.5,490.42,424.41,486.58,423.38,485.22Z"/>
<path class="cls-21" d="M420.05,491.3c-1.48-.31-4.19-2.14-7.87-3-1.41-.34-3.84-1.89-4.09-3.73-.32,2.11,2.52,4,4.09,4.36,3.68.89,6.39,2.73,7.87,3,1.1.23,2.52.38,3.4-.48a2.22,2.22,0,0,0,.48-.88A4.41,4.41,0,0,1,420.05,491.3Z"/>
<path class="cls-18" d="M435.18,479.92a7.35,7.35,0,0,1-5.56-.67c-1.34,0-8.62-3.08-9.61-.46-.86,2.29,2.29,4.41,4,4.82,3.67.89,6.39,2.72,7.87,3,1.1.23,2.52.38,3.4-.48C436.3,485.12,436.21,481.28,435.18,479.92Z"/>
<path class="cls-21" d="M431.85,486c-1.48-.31-4.2-2.14-7.87-3-1.41-.34-3.84-1.88-4.09-3.72-.32,2.11,2.52,4,4.09,4.36,3.67.89,6.39,2.72,7.87,3,1.1.23,2.52.38,3.4-.48a2.22,2.22,0,0,0,.48-.88A4.38,4.38,0,0,1,431.85,486Z"/>
<path id="_Контур_56" data-name="&lt;Контур&gt;" class="cls-22" d="M435.32,460.15a44.45,44.45,0,0,0-1.67-8s.5-6.88.75-13.4c.28-7.4,2.85-13.91.82-19.81l-22.82,5.07s1.38,29.5,1.68,33.07a66.94,66.94,0,0,0,1,8.28c1,5.73,2.73,19.35,2.73,19.35a8.52,8.52,0,0,0,5.58.62s-.17-17.22-.33-21.35c-.19-5-.34-4.65-.34-4.65l.76-12.55.49-6.24s.63,4.58,1,8.67c.36,3.57,1.22,6.8,2.18,13.44.84,5.76,2.19,17.34,2.19,17.34,2.42,1.4,5.69.24,5.69.24S436,465.15,435.32,460.15Z"/>
<path id="_Контур_57" data-name="&lt;Контур&gt;" class="cls-19" d="M432,383.88c-1.74.09-3,.54-3.25,0a24.73,24.73,0,0,1-.4-2.71c.2-.51.38-1.05.38-1.05,2.18-1.34,2.87-3.7,3.16-6.45.49-4.7-2.05-8.81-6.75-9.3-4.41-.46-7.66,2.65-8.53,6.91-.46,2.1-1.39,6.07-.14,9.53.71,1.94,1.61,3.47,2.52,3.77a20.09,20.09,0,0,0,2.61-.23h0s.23,1.3.39,2.14.1,1.28-1.57,2.3,2.52,3.14,5.26,2.93,5.81-1.86,6.66-3.62C433.27,386.15,433.18,383.83,432,383.88Z"/>
<path id="_Контур_58" data-name="&lt;Контур&gt;" class="cls-20" d="M429.48,384.14c.48,1.2-.75,2.67-4,3.7s-3.56-.3-3.56-.3a66.49,66.49,0,0,0-6.35,3.27c-2,1.37-2.82,5.34-3.09,12-.3,7.74-.33,19.23-.07,21.21,0,0,3.66,3.65,8.35,3.27s12.57-4.53,14.48-7.74c-.05-6.79-.85-7.89.25-11.8,2.52-9,3.84-12.62,2.62-18.57-1-4.91-2.42-5.52-4.36-5.47A41.07,41.07,0,0,0,429.48,384.14Z"/>
<path id="_Контур_59" data-name="&lt;Контур&gt;" class="cls-19" d="M405.1,409.24c3.85,1,4.75-1.45,5.26-3.46,1.22-4.86,1.88-9.25,2.79-12,1.08-3.26,2.2-3.78,3.79-4.41,1.85-.75,3.45.82,2.83,5a82.82,82.82,0,0,1-3.52,14.13c-.48,1.52-1.55,4.21-3.16,5.51-2,1.6-5,1.53-9.5.41-1.92-.48-4-1.63-7.69-3.33-1-.46-1.74-.76-3.62-1.64a12.94,12.94,0,0,1-4.82-3.76c-1-1.43-1.11-2.1-.91-2.42s.7-.32,1.22.28a10.72,10.72,0,0,0,2.77,2.56s-1.22-1.26-1.87-2.07a10.55,10.55,0,0,1-1.46-2.38c-.35-.83.37-2,.91-1.37s.81,1.2,1.8,2.39a10.35,10.35,0,0,0,1.78,1.7,19.53,19.53,0,0,1-1.64-2.64,4.45,4.45,0,0,1-.6-2.39c0-.43.65-.95,1.22-.12a16.2,16.2,0,0,0,2.07,3.15c.88.91,1.62,1.44,1.64,1.18s-.53-.75-.84-1.81,0-2.18.59-2.31.47-.09.94.91a28.49,28.49,0,0,1,1.75,3.17,6.56,6.56,0,0,0,1.58,2.73C399.36,407.21,401.45,408.32,405.1,409.24Z"/>
<path id="_Контур_60" data-name="&lt;Контур&gt;" class="cls-23" d="M418.36,388.69c-2.57-.37-4.38.78-5.52,4.67s-1.92,7.09-1.92,7.09a7,7,0,0,0,4.3,2.86c3.07.74,4.18-.73,4.18-.73s.85-4.08,1.38-6.85S421.54,389.16,418.36,388.69Z"/>
<path class="cls-18" d="M416.75,370.67s-4.05-6.16,4.74-7.42c6.34-.91,10.41,3.17,10.88,7.59.45,4.22-2.15,8.61-4,10.35a7.68,7.68,0,0,1-5.48-.67,33.14,33.14,0,0,1-.12-3.45S425.83,370.56,416.75,370.67Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Some files were not shown because too many files have changed in this diff Show more