diff --git a/backend/.dockerignore b/backend/.dockerignore index 4c49bd7..e69de29 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -1 +0,0 @@ -.env diff --git a/backend/Dockerfile b/backend/Dockerfile index 3fc3480..bdd94ff 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,12 +1,19 @@ -FROM golang:alpine +FROM golang:alpine AS build WORKDIR /app COPY . . RUN go mod download -RUN go mod tidy -RUN go build main.go +RUN go build -o /app/main . +RUN go build -o /app/migrator ./cmd/migrate -CMD ["./main"] +FROM alpine AS final + +WORKDIR /app + +COPY .env /app/ +COPY --from=build /app/main /app/migrator /app/cmd /app/ + +CMD ["/app/main"] diff --git a/backend/api/media/media.go b/backend/api/media/media.go index ab9a842..69caf0e 100644 --- a/backend/api/media/media.go +++ b/backend/api/media/media.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "net/http" + "os" "strconv" "fr.latosa-escrima/core" @@ -36,7 +37,8 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) { Model((*models.Media)(nil)). Count(context.Background()) - totalPages := int(math.Max(1, float64(total/limit))) + upperBound := float64(total) / float64(limit) + totalPages := int(math.Max(1, math.Ceil(upperBound))) var media []models.Media err = core.DB.NewSelect(). @@ -51,10 +53,20 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) { }.Respond(w, http.StatusInternalServerError) return } - baseURL := utils.GetURL(r) + scheme := "http" + if r.TLS != nil || os.Getenv("ENVIRONMENT") != "DEV" { // Check if the request is over HTTPS + scheme = "https" + } + + // Extract the host + host := r.Host + baseURL := fmt.Sprintf("%s://%s", scheme, host) + if os.Getenv("ENVIRONMENT") != "DEV" { + baseURL += "/api" + } media = utils.Map(media, func(m models.Media) models.Media { m.Author = nil - m.URL = fmt.Sprintf("%s%s/file", baseURL, m.ID) + m.URL = fmt.Sprintf("%s/media/%s/file", baseURL, m.ID) return m }) diff --git a/backend/api/media/update.go b/backend/api/media/update.go index 83e39a0..7a52895 100644 --- a/backend/api/media/update.go +++ b/backend/api/media/update.go @@ -1,3 +1,52 @@ package media -// TODO +import ( + "context" + "encoding/json" + "net/http" + + "fr.latosa-escrima/core" + "fr.latosa-escrima/core/models" + "github.com/google/uuid" +) + +func HandleUpdate(w http.ResponseWriter, r *http.Request) { + var media models.Media + err := json.NewDecoder(r.Body).Decode(&media) + if err != nil { + core.JSONError{ + Status: core.Error, + Message: err.Error(), + }.Respond(w, http.StatusBadRequest) + return + } + media_uuid := r.PathValue("media_uuid") + media.ID, err = uuid.Parse(media_uuid) + if err != nil { + core.JSONError{ + Status: core.Error, + Message: err.Error(), + }.Respond(w, http.StatusBadRequest) + return + } + + _, err = core.DB.NewUpdate(). + Model(&media). + OmitZero(). + WherePK(). + Exec(context.Background()) + if err != nil { + core.JSONError{ + Status: core.Error, + Message: err.Error(), + }.Respond(w, http.StatusInternalServerError) + return + } + + core.JSONSuccess{ + Status: core.Success, + Message: "Media updated", + Data: media, + }.Respond(w, http.StatusOK) + +} diff --git a/backend/api/media_routes.go b/backend/api/media_routes.go index 87db6f6..db5179b 100644 --- a/backend/api/media_routes.go +++ b/backend/api/media_routes.go @@ -14,7 +14,7 @@ var MediaRoutes = map[string]core.Handler{ Middlewares: []core.Middleware{Methods("POST"), AuthJWT}, }, // Paginated media response - "/media/": { + "/media": { Handler: media.HandleMedia, Middlewares: []core.Middleware{Methods("GET")}, }, @@ -28,10 +28,10 @@ var MediaRoutes = map[string]core.Handler{ Handler: media.HandleMediaFile, Middlewares: []core.Middleware{Methods("GET")}, }, - // "/media/{media_uuid}/update": { - // Handler: HandleGetMediaFile, - // Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT}, - // }, + "/media/{media_uuid}/update": { + Handler: media.HandleUpdate, + Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT}, + }, "/media/{media_uuid}/delete": { Handler: media.HandleDelete, Middlewares: []core.Middleware{Methods("DELETE"), AuthJWT}, diff --git a/backend/api/roles_routes.go b/backend/api/roles_routes.go index 9fc844f..bcc9ae9 100644 --- a/backend/api/roles_routes.go +++ b/backend/api/roles_routes.go @@ -6,7 +6,7 @@ import ( ) var RolesRoutes = map[string]core.Handler{ - "/roles/": { + "/roles": { Handler: roles.HandleRoles, Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, }, diff --git a/backend/api/shortcodes/shortcode.go b/backend/api/shortcodes/shortcode.go index 893a020..3ca69d3 100644 --- a/backend/api/shortcodes/shortcode.go +++ b/backend/api/shortcodes/shortcode.go @@ -2,7 +2,9 @@ package shortcodes import ( "context" + "fmt" "net/http" + "os" "fr.latosa-escrima/core" "fr.latosa-escrima/core/models" @@ -11,11 +13,21 @@ import ( func HandleShortcode(w http.ResponseWriter, r *http.Request) { code := r.PathValue("shortcode") var shortcode models.Shortcode - err := core.DB.NewSelect(). + count, err := core.DB.NewSelect(). Model(&shortcode). Where("code = ?", code). + Relation("Media"). Limit(1). - Scan(context.Background()) + ScanAndCount(context.Background()) + + if count == 0 { + core.JSONSuccess{ + Status: core.Success, + Message: "Shortcode has not been found", + }.Respond(w, http.StatusNotFound) + return + } + if err != nil { core.JSONError{ Status: core.Error, @@ -24,6 +36,20 @@ func HandleShortcode(w http.ResponseWriter, r *http.Request) { return } + scheme := "http" + if r.TLS != nil { // Check if the request is over HTTPS + scheme = "https" + } + + // Extract the host + host := r.Host + baseURL := fmt.Sprintf("%s://%s", scheme, host) + if os.Getenv("ENVIRONMENT") != "DEV" { + baseURL += "/api" + } + if shortcode.Media != nil { + shortcode.Media.URL = fmt.Sprintf("%s/media/%s/file", baseURL, shortcode.Media.ID) + } core.JSONSuccess{ Status: core.Success, Message: "Shortcode found", diff --git a/backend/api/shortcodes_routes.go b/backend/api/shortcodes_routes.go index 2d5cbfd..08cb2ee 100644 --- a/backend/api/shortcodes_routes.go +++ b/backend/api/shortcodes_routes.go @@ -10,7 +10,7 @@ var ShortcodesRoutes = map[string]core.Handler{ Handler: shortcodes.HandleNew, Middlewares: []core.Middleware{Methods("POST"), AuthJWT}, }, - "/shortcodes/": { + "/shortcodes": { Handler: shortcodes.HandleShortcodes, Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, }, diff --git a/backend/cmd/migrate/migrations/20250205081449_add_events_title.down.sql b/backend/cmd/migrate/migrations/20250205081449_add_events_title.down.sql new file mode 100644 index 0000000..08f950c --- /dev/null +++ b/backend/cmd/migrate/migrations/20250205081449_add_events_title.down.sql @@ -0,0 +1 @@ +ALTER TABLE events DROP COLUMN title; diff --git a/backend/cmd/migrate/migrations/20250205081449_add_events_title.up.sql b/backend/cmd/migrate/migrations/20250205081449_add_events_title.up.sql new file mode 100644 index 0000000..d1da967 --- /dev/null +++ b/backend/cmd/migrate/migrations/20250205081449_add_events_title.up.sql @@ -0,0 +1 @@ +ALTER TABLE events ADD COLUMN title text not null default ''; diff --git a/backend/cmd/migrate/migrations/20250210084555_add_events_columns.go b/backend/cmd/migrate/migrations/20250210084555_add_events_columns.go new file mode 100644 index 0000000..35ea0c7 --- /dev/null +++ b/backend/cmd/migrate/migrations/20250210084555_add_events_columns.go @@ -0,0 +1,61 @@ +package migrations + +import ( + "context" + "fmt" + + "fr.latosa-escrima/core/models" + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [up migration] ") + _, err := db.NewAddColumn(). + Model((*models.Event)(nil)). + ColumnExpr("full_day BOOLEAN NOT NULL DEFAULT FALSE"). + Exec(ctx) + if err != nil { + return err + } + + // Add "is_visible" column + _, err = db.NewAddColumn(). + Model((*models.Event)(nil)). + ColumnExpr("is_visible BOOLEAN NOT NULL DEFAULT TRUE"). + Exec(ctx) + if err != nil { + return err + } + + // Add "rrule" column + _, err = db.NewAddColumn(). + Model((*models.Event)(nil)). + ColumnExpr("rrule TEXT"). + Exec(ctx) + return err + }, func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [down migration] ") + _, err := db.NewDropColumn(). + Model((*models.Event)(nil)). + Column("full_day"). + Exec(ctx) + if err != nil { + return err + } + + _, err = db.NewDropColumn(). + Model((*models.Event)(nil)). + Column("is_visible"). + Exec(ctx) + if err != nil { + return err + } + + _, err = db.NewDropColumn(). + Model((*models.Event)(nil)). + Column("rrule"). + Exec(ctx) + return err + }) +} diff --git a/backend/cmd/migrate/migrations/20250213101006_remove_status_events.go b/backend/cmd/migrate/migrations/20250213101006_remove_status_events.go new file mode 100644 index 0000000..31b4343 --- /dev/null +++ b/backend/cmd/migrate/migrations/20250213101006_remove_status_events.go @@ -0,0 +1,23 @@ +package migrations + +import ( + "context" + "fmt" + + "fr.latosa-escrima/core/models" + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [up migration] ") + _, err := db.NewDropColumn(). + Model((*models.Event)(nil)). + ColumnExpr("status"). + Exec(ctx) + return err + }, func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [down migration] ") + return nil + }) +} diff --git a/backend/core/models/events.go b/backend/core/models/events.go index fc5c9b4..ad3054b 100644 --- a/backend/core/models/events.go +++ b/backend/core/models/events.go @@ -11,11 +11,11 @@ type Event struct { bun.BaseModel `bun:"table:events"` EventID uuid.UUID `bun:"event_id,type:uuid,pk,default:gen_random_uuid()" json:"id"` - Title string `bun:"title,notnull" json:"title"` + Title string `bun:"title,notnull" json:"title"` CreationDate time.Time `bun:"creation_date,notnull,default:current_timestamp" json:"creationDate"` ScheduleStart time.Time `bun:"schedule_start,notnull" json:"start"` ScheduleEnd time.Time `bun:"schedule_end,notnull" json:"end"` - FullDay bool `bun:"full_day,notnull,default:false" json:"fullDay"` - IsVisible bool `bun:"is_visible,notnull,default:true" json:"isVisible"` - Rrule string `bun:"rrule" json:"rrule"` + FullDay bool `bun:"full_day,notnull,default:false" json:"fullDay"` + IsVisible bool `bun:"is_visible,notnull,default:true" json:"isVisible"` + Rrule string `bun:"rrule" json:"rrule"` } diff --git a/backend/utils/get_url.go b/backend/utils/get_url.go index d6bf868..b01948d 100644 --- a/backend/utils/get_url.go +++ b/backend/utils/get_url.go @@ -3,6 +3,7 @@ package utils import ( "fmt" "net/http" + "os" ) func GetURL(r *http.Request) string { @@ -17,7 +18,10 @@ func GetURL(r *http.Request) string { // Get the full request URI (path + query string) fullPath := r.URL.Path + if os.Getenv("ENVIRONMENT") != "DEVELOPMENT" { + fullPath += "/api/" + } + // Build the full request URL return fmt.Sprintf("%s://%s%s", scheme, host, fullPath) } - diff --git a/docker-compose.yaml b/docker-compose.yaml index f8839af..f5b80bc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,10 +1,11 @@ services: latosa-escrima.fr-frontend: container_name: latosa-frontend - # image: cems.dev:5000/latosa-escrima.fr:latest - build: - context: ./frontend/ - dockerfile: Dockerfile + image: cems.dev:5000/latosa-escrima.fr:latest + #build: + # context: ./frontend/ + # dockerfile: Dockerfile + # target: final depends_on: - latosa-escrima.fr-backend env_file: .env diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 33b45ca..1f1bce1 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -11,11 +11,11 @@ WORKDIR /app # Install dependencies based on the preferred package manager COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ RUN \ - if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ - elif [ -f package-lock.json ]; then npm ci; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ - else echo "Lockfile not found." && exit 1; \ - fi + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci --force; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi # Rebuild the source code only when needed @@ -30,11 +30,11 @@ COPY . . # ENV NEXT_TELEMETRY_DISABLED=1 RUN \ - if [ -f yarn.lock ]; then yarn run build; \ - elif [ -f package-lock.json ]; then npm run build; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ - else echo "Lockfile not found." && exit 1; \ - fi + if [ -f yarn.lock ]; then yarn run build; \ + elif [ -f package-lock.json ]; then npm run build; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ + else echo "Lockfile not found." && exit 1; \ + fi # Production image, copy all the files and run next FROM base AS runner diff --git a/frontend/app/(auth)/dashboard/blogs/new/page.tsx b/frontend/app/(auth)/dashboard/blogs/new/page.tsx new file mode 100644 index 0000000..a1aa279 --- /dev/null +++ b/frontend/app/(auth)/dashboard/blogs/new/page.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { Textarea } from "@/components/ui/textarea"; +import { useEffect, useRef, useState } from "react"; +import { marked } from "marked"; +import DOMPurify from "isomorphic-dompurify"; +import { Button } from "@/components/ui/button"; +import { Bold, Italic, Strikethrough, Underline } from "lucide-react"; + +enum Command { + Italic = "*", + Bold = "**", + Strikethrough = "~~", + Underline = "__", +} + +export default function NewBlog() { + const ref = useRef(null); + const [text, setText] = useState(""); + const [cursor, setCursor] = useState<{ line: number; column: number }>({ + line: 0, + column: 0, + }); + const [selection, setSelection] = useState<{ + start: number; + end: number; + } | null>(null); + + const getCursorPosition = ( + event: React.ChangeEvent, + ) => { + const textarea = event.target; + const text = textarea.value; + const cursorPos = textarea.selectionStart; + + const lines = text.substring(0, cursorPos).split("\n"); + const line = lines.length; // Current line number (1-based) + const column = lines[lines.length - 1].length + 1; // Current column (1-based) + + setCursor({ line, column }); + }; + + const onSelect = (event: React.ChangeEvent) => { + const { selectionStart, selectionEnd } = event.currentTarget; + if (selectionStart === selectionEnd) return; + + setSelection({ start: selectionStart, end: selectionEnd }); + }; + + useEffect(() => { + setSelection(null); + }, [text]); + + const moveCursor = (newPos: number) => { + if (!ref.current) return; + ref.current.selectionEnd = newPos; + ref.current.focus(); + }; + + const execCommand = (command: Command, symetry: boolean = true) => { + if (selection) { + const selectedText = text.substring(selection.start, selection.end); + const pre = text.slice(0, selection.start); + const post = text.slice(selection.end); + const newSelectedText = `${command}${selectedText}${symetry ? command : ""}`; + setText(pre + newSelectedText + post); + return; + } + + const pre = text.slice(0, cursor.column); + const post = text.slice(cursor.column); + + if (!symetry) setText(pre + command + post); + else { + const t = pre + command + command + post; + setText(t); + } + console.log(pre.length + command.length); + moveCursor(cursor.column + 2); + }; + + const sanitized = DOMPurify.sanitize(marked(text, { async: false })); + + return ( +
+
+
+ + + + + {/* */} +
+