Shortcodes
This commit is contained in:
@@ -36,7 +36,8 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) {
|
|||||||
Model((*models.Media)(nil)).
|
Model((*models.Media)(nil)).
|
||||||
Count(context.Background())
|
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
|
var media []models.Media
|
||||||
err = core.DB.NewSelect().
|
err = core.DB.NewSelect().
|
||||||
|
|||||||
@@ -1,3 +1,52 @@
|
|||||||
package media
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ var MediaRoutes = map[string]core.Handler{
|
|||||||
Handler: media.HandleMediaFile,
|
Handler: media.HandleMediaFile,
|
||||||
Middlewares: []core.Middleware{Methods("GET")},
|
Middlewares: []core.Middleware{Methods("GET")},
|
||||||
},
|
},
|
||||||
// "/media/{media_uuid}/update": {
|
"/media/{media_uuid}/update": {
|
||||||
// Handler: HandleGetMediaFile,
|
Handler: media.HandleUpdate,
|
||||||
// Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT},
|
||||||
// },
|
},
|
||||||
"/media/{media_uuid}/delete": {
|
"/media/{media_uuid}/delete": {
|
||||||
Handler: media.HandleDelete,
|
Handler: media.HandleDelete,
|
||||||
Middlewares: []core.Middleware{Methods("DELETE"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("DELETE"), AuthJWT},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package shortcodes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"fr.latosa-escrima/core"
|
"fr.latosa-escrima/core"
|
||||||
@@ -14,6 +15,7 @@ func HandleShortcode(w http.ResponseWriter, r *http.Request) {
|
|||||||
err := core.DB.NewSelect().
|
err := core.DB.NewSelect().
|
||||||
Model(&shortcode).
|
Model(&shortcode).
|
||||||
Where("code = ?", code).
|
Where("code = ?", code).
|
||||||
|
Relation("Media").
|
||||||
Limit(1).
|
Limit(1).
|
||||||
Scan(context.Background())
|
Scan(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -24,6 +26,17 @@ func HandleShortcode(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 shortcode.Media != nil {
|
||||||
|
shortcode.Media.URL = fmt.Sprintf("%s/media/%s/file", baseURL, shortcode.Media.ID)
|
||||||
|
}
|
||||||
core.JSONSuccess{
|
core.JSONSuccess{
|
||||||
Status: core.Success,
|
Status: core.Success,
|
||||||
Message: "Shortcode found",
|
Message: "Shortcode found",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE events DROP COLUMN title;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE events ADD COLUMN title text not null default '';
|
||||||
@@ -18,7 +18,7 @@ type Event struct {
|
|||||||
bun.BaseModel `bun:"table:events"`
|
bun.BaseModel `bun:"table:events"`
|
||||||
|
|
||||||
EventID uuid.UUID `bun:"event_id,type:uuid,pk,default:gen_random_uuid()" json:"id"`
|
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"`
|
CreationDate time.Time `bun:"creation_date,notnull,default:current_timestamp" json:"creationDate"`
|
||||||
ScheduleStart time.Time `bun:"schedule_start,notnull" json:"start"`
|
ScheduleStart time.Time `bun:"schedule_start,notnull" json:"start"`
|
||||||
ScheduleEnd time.Time `bun:"schedule_end,notnull" json:"end"`
|
ScheduleEnd time.Time `bun:"schedule_end,notnull" json:"end"`
|
||||||
|
|||||||
141
frontend/app/(auth)/dashboard/blogs/new/page.tsx
Normal file
141
frontend/app/(auth)/dashboard/blogs/new/page.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { marked } from "marked";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Bold, Italic, Link, Strikethrough, Underline } from "lucide-react";
|
||||||
|
|
||||||
|
enum Command {
|
||||||
|
Italic = "*",
|
||||||
|
Bold = "**",
|
||||||
|
Strikethrough = "~~",
|
||||||
|
Underline = "__",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewBlog() {
|
||||||
|
const ref = useRef<HTMLTextAreaElement>(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<HTMLTextAreaElement>,
|
||||||
|
) => {
|
||||||
|
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<HTMLTextAreaElement>) => {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="flex">
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-2 mb-2 border-b pb-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => execCommand(Command.Bold)}
|
||||||
|
>
|
||||||
|
<Bold size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => execCommand(Command.Italic)}
|
||||||
|
>
|
||||||
|
<Italic size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => execCommand(Command.Underline)}
|
||||||
|
>
|
||||||
|
<Underline size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => execCommand(Command.Strikethrough)}
|
||||||
|
>
|
||||||
|
<Strikethrough size={16} />
|
||||||
|
</Button>
|
||||||
|
{/*<Button variant="outline" size="icon" onClick={handleLink}>
|
||||||
|
<Link size={16} />
|
||||||
|
</Button> */}
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
ref={ref}
|
||||||
|
value={text}
|
||||||
|
onSelect={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
getCursorPosition(e);
|
||||||
|
onSelect(e);
|
||||||
|
}}
|
||||||
|
onChange={(e) => setText(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
Line: {cursor.line}; Column: {cursor.column}
|
||||||
|
<br />
|
||||||
|
Selection: Start {selection?.start} End {selection?.end}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mt-4 p-2 bg-gray-100 border rounded-md text-sm text-black"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(marked(text, { async: false })),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -123,7 +123,7 @@ export default function UserDetailsPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectValue placeholder="Select an organization" />
|
<SelectValue placeholder="Sélectionner un rôle" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{availableRoles.data
|
{availableRoles.data
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import useFileUpload from "@/hooks/use-file-upload";
|
|
||||||
import { ChangeEvent } from "react";
|
|
||||||
|
|
||||||
const MyComponent = () => {
|
|
||||||
const { progress, isUploading, error, uploadFile, cancelUpload } =
|
|
||||||
useFileUpload();
|
|
||||||
|
|
||||||
const handleFileUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = event.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
uploadFile(file, "/media/upload", (response) => {
|
|
||||||
console.log("Upload success:", response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input type="file" onChange={handleFileUpload} />
|
|
||||||
{isUploading && <p>Uploading... {progress}%</p>}
|
|
||||||
{error && <p>Error: {error}</p>}
|
|
||||||
<button onClick={cancelUpload} disabled={!isUploading}>
|
|
||||||
Cancel Upload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MyComponent;
|
|
||||||
@@ -16,7 +16,6 @@ import { PhotoDialog } from "@/components/photo-dialog";
|
|||||||
import useFileUpload from "@/hooks/use-file-upload";
|
import useFileUpload from "@/hooks/use-file-upload";
|
||||||
import useMedia from "@/hooks/use-media";
|
import useMedia from "@/hooks/use-media";
|
||||||
import Media from "@/interfaces/Media";
|
import Media from "@/interfaces/Media";
|
||||||
import useApiMutation from "@/hooks/use-api";
|
|
||||||
import request from "@/lib/request";
|
import request from "@/lib/request";
|
||||||
|
|
||||||
export default function PhotoGallery() {
|
export default function PhotoGallery() {
|
||||||
@@ -41,8 +40,22 @@ export default function PhotoGallery() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatePhoto = (updatedPhoto: Omit<Media, "id">) => {
|
const handleUpdatePhoto = async (
|
||||||
|
body: Media | Omit<Media, "id">,
|
||||||
|
file: File,
|
||||||
|
) => {
|
||||||
if (selectedPhoto) {
|
if (selectedPhoto) {
|
||||||
|
const res = await request<Media>(
|
||||||
|
`/media/${selectedPhoto.id}/update`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
requiresAuth: true,
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.status === "Success") {
|
||||||
|
mutate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setSelectedPhoto(null);
|
setSelectedPhoto(null);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Testimonial from "@/components/testimonial";
|
|||||||
import { CarouselItem } from "@/components/ui/carousel";
|
import { CarouselItem } from "@/components/ui/carousel";
|
||||||
import YouTubeEmbed from "@/components/youtube-embed";
|
import YouTubeEmbed from "@/components/youtube-embed";
|
||||||
import { IYoutube } from "@/interfaces/youtube";
|
import { IYoutube } from "@/interfaces/youtube";
|
||||||
|
import getShortcode from "@/lib/getShortcode";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
let videos: IYoutube | null = null;
|
let videos: IYoutube | null = null;
|
||||||
@@ -15,9 +16,18 @@ export default async function Home() {
|
|||||||
const res = await fetch(query);
|
const res = await fetch(query);
|
||||||
videos = await res.json();
|
videos = await res.json();
|
||||||
}
|
}
|
||||||
|
const hero = await getShortcode("hero_image");
|
||||||
|
const systemEvolution = await getShortcode("evolution_systeme");
|
||||||
|
const fondations = await getShortcode("fondements");
|
||||||
|
const todaysPrinciples = await getShortcode("aujourdhui");
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<Hero />
|
<Hero
|
||||||
|
background={
|
||||||
|
hero.media?.url ??
|
||||||
|
"https://shadcnblocks.com/images/block/placeholder-2.svg"
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div className="p-12">
|
<div className="p-12">
|
||||||
<YouTubeEmbed
|
<YouTubeEmbed
|
||||||
loadIframe
|
loadIframe
|
||||||
@@ -36,7 +46,10 @@ export default async function Home() {
|
|||||||
<FeatureItem
|
<FeatureItem
|
||||||
title="Les Fondements de Latosa Escrima Concepts"
|
title="Les Fondements de Latosa Escrima Concepts"
|
||||||
position="left"
|
position="left"
|
||||||
image="https://shadcnblocks.com/images/block/placeholder-2.svg"
|
image={
|
||||||
|
fondations.media?.url ??
|
||||||
|
"https://shadcnblocks.com/images/block/placeholder-2.svg"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ol className="flex list-decimal flex-col gap-4 text-justify">
|
<ol className="flex list-decimal flex-col gap-4 text-justify">
|
||||||
<li>
|
<li>
|
||||||
@@ -72,7 +85,10 @@ export default async function Home() {
|
|||||||
<FeatureItem
|
<FeatureItem
|
||||||
title="L’Évolution du Système"
|
title="L’Évolution du Système"
|
||||||
position="right"
|
position="right"
|
||||||
image="https://shadcnblocks.com/images/block/placeholder-2.svg"
|
image={
|
||||||
|
systemEvolution.media?.url ??
|
||||||
|
"https://shadcnblocks.com/images/block/placeholder-2.svg"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ol className="flex list-none flex-col gap-4 text-justify">
|
<ol className="flex list-none flex-col gap-4 text-justify">
|
||||||
<li>
|
<li>
|
||||||
@@ -117,7 +133,10 @@ export default async function Home() {
|
|||||||
<FeatureItem
|
<FeatureItem
|
||||||
title="Les Principes du Système Aujourd’hui"
|
title="Les Principes du Système Aujourd’hui"
|
||||||
position="left"
|
position="left"
|
||||||
image="https://shadcnblocks.com/images/block/placeholder-2.svg"
|
image={
|
||||||
|
todaysPrinciples.media?.url ??
|
||||||
|
"https://shadcnblocks.com/images/block/placeholder-2.svg"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Latosa Escrima Concepts repose sur cinq concepts
|
Latosa Escrima Concepts repose sur cinq concepts
|
||||||
fondamentaux :
|
fondamentaux :
|
||||||
|
|||||||
@@ -5,19 +5,18 @@ import Link from "next/link";
|
|||||||
import { API_URL } from "@/lib/constants";
|
import { API_URL } from "@/lib/constants";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const Hero = () => {
|
const Hero: React.FC<{ background: string }> = ({ background }) => {
|
||||||
const background = `${API_URL}/media/591ab183-c72d-46ff-905c-ec04fed1bb34/file`;
|
|
||||||
return (
|
return (
|
||||||
<section className="relative flex h-[calc(100vh-68px)] items-center justify-center overflow-hidden py-32">
|
<section className="relative flex h-[calc(100vh-68px)] items-center justify-center overflow-hidden py-32">
|
||||||
<div className="">
|
<div className="">
|
||||||
<Image
|
<Image
|
||||||
src={background}
|
src={background}
|
||||||
layout="fill"
|
layout="fill"
|
||||||
objectFit="cover"
|
// objectFit="cover"
|
||||||
priority
|
priority
|
||||||
alt="Hero image"
|
alt="Hero image"
|
||||||
unoptimized
|
unoptimized
|
||||||
className="grayscale"
|
className="grayscale object-cover "
|
||||||
/>
|
/>
|
||||||
{/* Gradient and Blur Overlay */}
|
{/* Gradient and Blur Overlay */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-transparent to-transparent bg-opacity-30 backdrop-blur-sm"></div>
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-transparent to-transparent bg-opacity-30 backdrop-blur-sm"></div>
|
||||||
@@ -29,10 +28,12 @@ const Hero = () => {
|
|||||||
className="h-16"
|
className="h-16"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-6 text-pretty text-2xl font-bold text-primary lg:text-5xl">
|
<h1 className="mb-6 text-pretty text-2xl font-bold text-primary lg:text-5xl font-times">
|
||||||
Trouvez votre équilibre avec
|
Trouvez votre <em>équilibre</em> avec
|
||||||
<br />
|
<br />
|
||||||
Latosa-Escrima
|
<span className="font-extrabold text-3xl lg:text-6xl">
|
||||||
|
Latosa Escrima
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground lg:text-xl">
|
<p className="text-muted-foreground lg:text-xl">
|
||||||
Une évolution des arts martiaux Philippins
|
Une évolution des arts martiaux Philippins
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface PhotoDialogProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onDelete?: (id: Media["id"]) => void;
|
onDelete?: (id: Media["id"]) => void;
|
||||||
onSave: (photo: Omit<Media, "id">, file: File) => void;
|
onSave: (photo: Omit<Media, "id"> | Media, file: File) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PhotoDialog({
|
export function PhotoDialog({
|
||||||
@@ -34,7 +34,7 @@ export function PhotoDialog({
|
|||||||
onSave,
|
onSave,
|
||||||
}: PhotoDialogProps) {
|
}: PhotoDialogProps) {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [newPhoto, setNewPhoto] = useState<Omit<Media, "id">>({
|
const [newPhoto, setNewPhoto] = useState<Omit<Media, "id"> | Media>({
|
||||||
url: "",
|
url: "",
|
||||||
alt: "",
|
alt: "",
|
||||||
path: "",
|
path: "",
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import type IShortcode from "@/interfaces/IShortcode";
|
import type IShortcode from "@/interfaces/IShortcode";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Media from "@/interfaces/Media";
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "./ui/pagination";
|
||||||
|
import useMedia from "@/hooks/use-media";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
interface ShortcodeDialogProps {
|
interface ShortcodeDialogProps {
|
||||||
onSave: (shortcode: IShortcode) => void;
|
onSave: (shortcode: IShortcode) => void;
|
||||||
@@ -33,6 +45,8 @@ export default function ShortcodeDialog({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
if (!(_shortcode?.code && (_shortcode.media_id || _shortcode.value)))
|
||||||
|
return;
|
||||||
onSave(_shortcode);
|
onSave(_shortcode);
|
||||||
setOpen();
|
setOpen();
|
||||||
resetForm();
|
resetForm();
|
||||||
@@ -92,6 +106,7 @@ export default function ShortcodeDialog({
|
|||||||
setShortcode((p) => ({
|
setShortcode((p) => ({
|
||||||
...p,
|
...p,
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
|
media_id: undefined,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className="col-span-3"
|
className="col-span-3"
|
||||||
@@ -99,27 +114,29 @@ export default function ShortcodeDialog({
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="media">
|
<TabsContent value="media">
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<PhotoGrid
|
||||||
<Label htmlFor="mediaId" className="text-right">
|
onSelect={(photo) => {
|
||||||
Media ID
|
setShortcode((p) => ({
|
||||||
</Label>
|
...p,
|
||||||
<Input
|
media_id: photo.id,
|
||||||
id="mediaId"
|
value: undefined,
|
||||||
value={_shortcode.media_id}
|
}));
|
||||||
onChange={(e) =>
|
}}
|
||||||
setShortcode((p) => ({
|
/>
|
||||||
...p,
|
|
||||||
media_id: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="col-span-3"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" onClick={handleSave}>
|
<Button
|
||||||
|
disabled={
|
||||||
|
!(
|
||||||
|
_shortcode?.code &&
|
||||||
|
(_shortcode.media_id || _shortcode.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="submit"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -127,3 +144,144 @@ export default function ShortcodeDialog({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LazyImage = ({
|
||||||
|
photo,
|
||||||
|
onClick,
|
||||||
|
isSelected,
|
||||||
|
}: {
|
||||||
|
photo: Media;
|
||||||
|
onClick: () => void;
|
||||||
|
isSelected: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`aspect-square ${isSelected ? "ring-4 ring-primary" : ""}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={photo.url || "/placeholder.svg"}
|
||||||
|
alt={photo.alt}
|
||||||
|
width={300}
|
||||||
|
height={300}
|
||||||
|
className="object-cover w-full h-full cursor-pointer transition-transform hover:scale-105"
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PhotoGrid: React.FC<{ onSelect: (photo: Media) => void }> = ({
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
const { data, error, isLoading, success, setPage, setLimit, mutate } =
|
||||||
|
useMedia(5);
|
||||||
|
const [selectedPhoto, setSelectedPhoto] = useState<Media | null>(null);
|
||||||
|
|
||||||
|
const handlePhotoClick = (photo: Media) => {
|
||||||
|
setSelectedPhoto(photo);
|
||||||
|
onSelect(photo);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeSelection = () => {
|
||||||
|
setSelectedPhoto(null);
|
||||||
|
// if (!photos) return;
|
||||||
|
// const currentIndex = photos.findIndex(
|
||||||
|
// (photo) => photo.id === selectedPhoto?.id,
|
||||||
|
// );
|
||||||
|
// const nextIndex = (currentIndex + 1) % photos.length;
|
||||||
|
// setSelectedPhoto(photos[nextIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <Loader2 className="animate-spin" />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Gallerie Photo</h1>
|
||||||
|
{selectedPhoto ? (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<Image
|
||||||
|
src={selectedPhoto.url || "/placeholder.svg"}
|
||||||
|
alt={selectedPhoto.alt}
|
||||||
|
width={600}
|
||||||
|
height={600}
|
||||||
|
className="w-full max-w-2xl h-auto mb-4 rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-4 mt-4">
|
||||||
|
<Button onClick={handleChangeSelection}>
|
||||||
|
Changer de sélection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||||
|
{data?.items?.map((photo) => {
|
||||||
|
return (
|
||||||
|
<LazyImage
|
||||||
|
photo={photo}
|
||||||
|
key={photo.id}
|
||||||
|
onClick={() => handlePhotoClick(photo)}
|
||||||
|
isSelected={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination className="mt-8">
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setPage((prev) =>
|
||||||
|
Math.max(prev - 1, 1),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
data?.page === 1
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
{[...Array(data?.totalPages)].map((_, i) => (
|
||||||
|
<PaginationItem key={i + 1}>
|
||||||
|
<PaginationLink
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setPage(i + 1);
|
||||||
|
}}
|
||||||
|
isActive={data?.page === i + 1}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
))}
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setPage((prev) =>
|
||||||
|
Math.min(
|
||||||
|
prev + 1,
|
||||||
|
data?.totalPages ?? 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
data?.page === data?.totalPages
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
13
frontend/lib/getShortcode.ts
Normal file
13
frontend/lib/getShortcode.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import IShortcode from "@/interfaces/IShortcode";
|
||||||
|
import request from "./request";
|
||||||
|
|
||||||
|
export default async function getShortcode(code: string): Promise<IShortcode> {
|
||||||
|
const res = await request<IShortcode>(`/shortcodes/${code}`, {
|
||||||
|
method: "GET",
|
||||||
|
requiresAuth: false,
|
||||||
|
});
|
||||||
|
if (res.status === "Error" || !res.data)
|
||||||
|
throw new Error("Shortcode doesn't exist.");
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ export default async function request<T>(
|
|||||||
cookies?: () => Promise<ReadonlyRequestCookies>;
|
cookies?: () => Promise<ReadonlyRequestCookies>;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<ApiResponse<T>> {
|
): Promise<ApiResponse<T>> {
|
||||||
console.log("Hello everyone");
|
|
||||||
const { method = "GET", body, requiresAuth = true } = options;
|
const { method = "GET", body, requiresAuth = true } = options;
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@@ -37,8 +37,10 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookies-next": "^5.1.0",
|
"cookies-next": "^5.1.0",
|
||||||
"date-fns": "^3.0.0",
|
"date-fns": "^3.0.0",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
"embla-carousel-react": "^8.5.2",
|
"embla-carousel-react": "^8.5.2",
|
||||||
"lucide-react": "^0.471.1",
|
"lucide-react": "^0.471.1",
|
||||||
|
"marked": "^15.0.6",
|
||||||
"next": "15.1.4",
|
"next": "15.1.4",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -2125,6 +2127,13 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.19.1",
|
"version": "8.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz",
|
||||||
@@ -3208,6 +3217,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -5116,6 +5134,18 @@
|
|||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "15.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz",
|
||||||
|
"integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|||||||
@@ -38,8 +38,10 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookies-next": "^5.1.0",
|
"cookies-next": "^5.1.0",
|
||||||
"date-fns": "^3.0.0",
|
"date-fns": "^3.0.0",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
"embla-carousel-react": "^8.5.2",
|
"embla-carousel-react": "^8.5.2",
|
||||||
"lucide-react": "^0.471.1",
|
"lucide-react": "^0.471.1",
|
||||||
|
"marked": "^15.0.6",
|
||||||
"next": "15.1.4",
|
"next": "15.1.4",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export default {
|
|||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
times: ["Times New Roman", "Times", "serif"],
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
background: "hsl(var(--background))",
|
background: "hsl(var(--background))",
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: "hsl(var(--foreground))",
|
||||||
|
|||||||
Reference in New Issue
Block a user