Shortcodes
This commit is contained in:
@@ -36,7 +36,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().
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -2,6 +2,7 @@ package shortcodes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"fr.latosa-escrima/core"
|
||||
@@ -14,6 +15,7 @@ func HandleShortcode(w http.ResponseWriter, r *http.Request) {
|
||||
err := core.DB.NewSelect().
|
||||
Model(&shortcode).
|
||||
Where("code = ?", code).
|
||||
Relation("Media").
|
||||
Limit(1).
|
||||
Scan(context.Background())
|
||||
if err != nil {
|
||||
@@ -24,6 +26,17 @@ 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 shortcode.Media != nil {
|
||||
shortcode.Media.URL = fmt.Sprintf("%s/media/%s/file", baseURL, shortcode.Media.ID)
|
||||
}
|
||||
core.JSONSuccess{
|
||||
Status: core.Success,
|
||||
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"`
|
||||
|
||||
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"`
|
||||
|
||||
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]">
|
||||
<SelectValue placeholder="Select an organization" />
|
||||
<SelectValue placeholder="Sélectionner un rôle" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{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 useMedia from "@/hooks/use-media";
|
||||
import Media from "@/interfaces/Media";
|
||||
import useApiMutation from "@/hooks/use-api";
|
||||
import request from "@/lib/request";
|
||||
|
||||
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) {
|
||||
const res = await request<Media>(
|
||||
`/media/${selectedPhoto.id}/update`,
|
||||
{
|
||||
method: "PATCH",
|
||||
requiresAuth: true,
|
||||
body,
|
||||
},
|
||||
);
|
||||
if (res.status === "Success") {
|
||||
mutate();
|
||||
}
|
||||
}
|
||||
setSelectedPhoto(null);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import Testimonial from "@/components/testimonial";
|
||||
import { CarouselItem } from "@/components/ui/carousel";
|
||||
import YouTubeEmbed from "@/components/youtube-embed";
|
||||
import { IYoutube } from "@/interfaces/youtube";
|
||||
import getShortcode from "@/lib/getShortcode";
|
||||
|
||||
export default async function Home() {
|
||||
let videos: IYoutube | null = null;
|
||||
@@ -15,9 +16,18 @@ export default async function Home() {
|
||||
const res = await fetch(query);
|
||||
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 (
|
||||
<main>
|
||||
<Hero />
|
||||
<Hero
|
||||
background={
|
||||
hero.media?.url ??
|
||||
"https://shadcnblocks.com/images/block/placeholder-2.svg"
|
||||
}
|
||||
/>
|
||||
<div className="p-12">
|
||||
<YouTubeEmbed
|
||||
loadIframe
|
||||
@@ -36,7 +46,10 @@ export default async function Home() {
|
||||
<FeatureItem
|
||||
title="Les Fondements de Latosa Escrima Concepts"
|
||||
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">
|
||||
<li>
|
||||
@@ -72,7 +85,10 @@ export default async function Home() {
|
||||
<FeatureItem
|
||||
title="L’Évolution du Système"
|
||||
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">
|
||||
<li>
|
||||
@@ -117,7 +133,10 @@ export default async function Home() {
|
||||
<FeatureItem
|
||||
title="Les Principes du Système Aujourd’hui"
|
||||
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
|
||||
fondamentaux :
|
||||
|
||||
@@ -5,19 +5,18 @@ import Link from "next/link";
|
||||
import { API_URL } from "@/lib/constants";
|
||||
import Image from "next/image";
|
||||
|
||||
const Hero = () => {
|
||||
const background = `${API_URL}/media/591ab183-c72d-46ff-905c-ec04fed1bb34/file`;
|
||||
const Hero: React.FC<{ background: string }> = ({ background }) => {
|
||||
return (
|
||||
<section className="relative flex h-[calc(100vh-68px)] items-center justify-center overflow-hidden py-32">
|
||||
<div className="">
|
||||
<Image
|
||||
src={background}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
// objectFit="cover"
|
||||
priority
|
||||
alt="Hero image"
|
||||
unoptimized
|
||||
className="grayscale"
|
||||
className="grayscale object-cover "
|
||||
/>
|
||||
{/* 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>
|
||||
@@ -29,10 +28,12 @@ const Hero = () => {
|
||||
className="h-16"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="mb-6 text-pretty text-2xl font-bold text-primary lg:text-5xl">
|
||||
Trouvez votre équilibre avec
|
||||
<h1 className="mb-6 text-pretty text-2xl font-bold text-primary lg:text-5xl font-times">
|
||||
Trouvez votre <em>équilibre</em> avec
|
||||
<br />
|
||||
Latosa-Escrima
|
||||
<span className="font-extrabold text-3xl lg:text-6xl">
|
||||
Latosa Escrima
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground lg:text-xl">
|
||||
Une évolution des arts martiaux Philippins
|
||||
|
||||
@@ -23,7 +23,7 @@ interface PhotoDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => 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({
|
||||
@@ -34,7 +34,7 @@ export function PhotoDialog({
|
||||
onSave,
|
||||
}: PhotoDialogProps) {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [newPhoto, setNewPhoto] = useState<Omit<Media, "id">>({
|
||||
const [newPhoto, setNewPhoto] = useState<Omit<Media, "id"> | Media>({
|
||||
url: "",
|
||||
alt: "",
|
||||
path: "",
|
||||
|
||||
@@ -14,6 +14,18 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
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 {
|
||||
onSave: (shortcode: IShortcode) => void;
|
||||
@@ -33,6 +45,8 @@ export default function ShortcodeDialog({
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!(_shortcode?.code && (_shortcode.media_id || _shortcode.value)))
|
||||
return;
|
||||
onSave(_shortcode);
|
||||
setOpen();
|
||||
resetForm();
|
||||
@@ -92,6 +106,7 @@ export default function ShortcodeDialog({
|
||||
setShortcode((p) => ({
|
||||
...p,
|
||||
value: e.target.value,
|
||||
media_id: undefined,
|
||||
}))
|
||||
}
|
||||
className="col-span-3"
|
||||
@@ -99,27 +114,29 @@ export default function ShortcodeDialog({
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="media">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="mediaId" className="text-right">
|
||||
Media ID
|
||||
</Label>
|
||||
<Input
|
||||
id="mediaId"
|
||||
value={_shortcode.media_id}
|
||||
onChange={(e) =>
|
||||
setShortcode((p) => ({
|
||||
...p,
|
||||
media_id: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<PhotoGrid
|
||||
onSelect={(photo) => {
|
||||
setShortcode((p) => ({
|
||||
...p,
|
||||
media_id: photo.id,
|
||||
value: undefined,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" onClick={handleSave}>
|
||||
<Button
|
||||
disabled={
|
||||
!(
|
||||
_shortcode?.code &&
|
||||
(_shortcode.media_id || _shortcode.value)
|
||||
)
|
||||
}
|
||||
type="submit"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Enregistrer
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -127,3 +144,144 @@ export default function ShortcodeDialog({
|
||||
</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>;
|
||||
} = {},
|
||||
): Promise<ApiResponse<T>> {
|
||||
console.log("Hello everyone");
|
||||
const { method = "GET", body, requiresAuth = true } = options;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@@ -37,8 +37,10 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cookies-next": "^5.1.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"lucide-react": "^0.471.1",
|
||||
"marked": "^15.0.6",
|
||||
"next": "15.1.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.0.0",
|
||||
@@ -2125,6 +2127,13 @@
|
||||
"@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": {
|
||||
"version": "8.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz",
|
||||
@@ -3208,6 +3217,15 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -38,8 +38,10 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cookies-next": "^5.1.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"lucide-react": "^0.471.1",
|
||||
"marked": "^15.0.6",
|
||||
"next": "15.1.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -9,6 +9,9 @@ export default {
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
times: ["Times New Roman", "Times", "serif"],
|
||||
},
|
||||
colors: {
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
|
||||
Reference in New Issue
Block a user