Shortcodes
This commit is contained in:
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 :
|
||||
|
||||
Reference in New Issue
Block a user