mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
parent
851d04dfac
commit
fefe278d79
10 changed files with 182 additions and 234 deletions
|
|
@ -7,6 +7,7 @@ describe('Editor Component', () => {
|
|||
const mockOnEditorChange = jest.fn();
|
||||
|
||||
const sampleProps = {
|
||||
label: 'Sample Label',
|
||||
initialValue: 'Sample Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
|
@ -29,6 +30,7 @@ describe('Editor Component', () => {
|
|||
|
||||
it('updates editor value when initialValue prop changes', () => {
|
||||
const updatedProps = {
|
||||
label: 'Updated Label',
|
||||
initialValue: 'Updated Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
|
@ -43,6 +45,7 @@ describe('Editor Component', () => {
|
|||
|
||||
test('should call change text with the correct value on textarea change', () => {
|
||||
const updatedProps = {
|
||||
label: 'Updated Label',
|
||||
initialValue: 'Updated Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
|
@ -58,6 +61,7 @@ describe('Editor Component', () => {
|
|||
|
||||
test('should call onEditorChange with an empty string if textarea value is falsy', () => {
|
||||
const updatedProps = {
|
||||
label: 'Updated Label',
|
||||
initialValue: 'Updated Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ describe('QuizForm Component', () => {
|
|||
);
|
||||
|
||||
expect(screen.queryByText('Éditeur de quiz')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Éditeur')).toBeInTheDocument();
|
||||
// expect(screen.queryByText('Éditeur')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Prévisualisation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
// Editor.tsx
|
||||
import React, { useState, useRef } from 'react';
|
||||
import './editor.css';
|
||||
import { TextareaAutosize } from '@mui/material';
|
||||
|
||||
interface EditorProps {
|
||||
label: string;
|
||||
initialValue: string;
|
||||
onEditorChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange }) => {
|
||||
const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange, label }) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const editorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
|
|
@ -18,14 +20,17 @@ const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<textarea
|
||||
<label>
|
||||
<h4>{label}</h4>
|
||||
<TextareaAutosize
|
||||
id="editor-textarea"
|
||||
ref={editorRef}
|
||||
onChange={handleEditorChange}
|
||||
value={value}
|
||||
className="editor"
|
||||
></textarea>
|
||||
</div>
|
||||
minRows={5}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,18 +21,18 @@ const GiftCheatSheet: React.FC = () => {
|
|||
};
|
||||
|
||||
|
||||
const QuestionVraiFaux = "2+2 \\= 4 ? {T\n}// Vous pouvez utiliser les valeurs {T}, {F}, {TRUE} et {FALSE}";
|
||||
const QuestionVraiFaux = "2+2 \\= 4 ? {T\n}// Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}";
|
||||
const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}// La bonne réponse est Ottawa";
|
||||
const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n~ %33.3% Ottawa \n~ %33.3% Vancouver \n~ %-100% New York \n~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n} //On utilise le signe ~ pour toutes les réponses. On doit indiquer le pourcentage de chaque réponse";
|
||||
const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}// Permet de fournir plusieurs bonnes réponses. Note: Les majuscules ne sont pas prises en compte.";
|
||||
const QuestionNum ="Question {#=Nombre\n} //OU \nQuestion {#=Nombre:Tolérance\n} //OU \nQuestion {#=PetitNombre..GrandNombre\n} // La tolérance est un pourcentage. La réponse doit être comprise entre PetitNombre et GrandNombre";
|
||||
const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n// Utilisez le signe ~ pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse.";
|
||||
const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n// Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte.";
|
||||
const QuestionNum ="Question {#=Nombre\n} //OU \nQuestion {#=Nombre:Tolérance\n} // OU \nQuestion {#=PetitNombre..GrandNombre\n}\n// La tolérance est un pourcentage.\n// La réponse doit être comprise entre PetitNombre et GrandNombre";
|
||||
return (
|
||||
<div className="gift-cheat-sheet">
|
||||
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
|
||||
<span>
|
||||
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
|
||||
plateforme Moodle afin de générer les quizs. Ci-dessous vous pouvez retrouver la
|
||||
syntaxe pour chaque type de question ainsi que les champs optionnels :
|
||||
plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la
|
||||
syntaxe pour chaque type de question :
|
||||
</span>
|
||||
<div className="question-type">
|
||||
<h4>1. Questions Vrai/Faux</h4>
|
||||
|
|
@ -69,7 +69,7 @@ const GiftCheatSheet: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4>4. Questions à reponse courte</h4>
|
||||
<h4>4. Questions à réponse courte</h4>
|
||||
<pre>
|
||||
<code className="question-code-block selectable-text">
|
||||
{QuestionCourte}
|
||||
|
|
@ -79,7 +79,7 @@ const GiftCheatSheet: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4> 5. Questions numériques </h4>
|
||||
<h4> 5. Question numérique </h4>
|
||||
<pre>
|
||||
<code className="question-code-block selectable-text">
|
||||
{
|
||||
|
|
@ -139,7 +139,7 @@ const GiftCheatSheet: React.FC = () => {
|
|||
<div className="question-type">
|
||||
<h4> 8. LaTeX et Markdown</h4>
|
||||
<p>
|
||||
Les format LaTeX et markdown sont supportés dans cette application. Vous devez cependant penser
|
||||
Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser
|
||||
à 'échapper' les caractères spéciaux mentionnés plus haut.
|
||||
</p>
|
||||
<p>Exemple d'équation:</p>
|
||||
|
|
@ -154,18 +154,30 @@ const GiftCheatSheet: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4> 9. inserer une image </h4>
|
||||
<p>Pour insérer une image, vous devez utiliser la syntaxe suivante:</p>
|
||||
<h4> 9. Images </h4>
|
||||
<p>Pour insérer une image dans une question ou dans une réponse, vous devez utiliser la syntaxe suivante:</p>
|
||||
<pre>
|
||||
<code className="question-code-block">
|
||||
{'<img '}
|
||||
<span className="code-comment">{`un_URL_d_image`}</span>
|
||||
{' >'}
|
||||
{''}
|
||||
</code>
|
||||
</pre>
|
||||
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p>
|
||||
<pre>
|
||||
<code className="question-code-block">
|
||||
{'[markdown]Ceci est un chat: \n\n{T}'}
|
||||
</code>
|
||||
</pre>
|
||||
<p>Note : les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
|
||||
<p>Note : On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p>
|
||||
<p style={{ color: 'red' }}>
|
||||
Attention nous ne supportons pas encore les images en tant que réponses à une
|
||||
question
|
||||
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
|
||||
supportée.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -184,5 +196,4 @@ const GiftCheatSheet: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
export default GiftCheatSheet;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.gift-cheat-sheet {
|
||||
width: 30vw;
|
||||
/* width: 30vw; */
|
||||
height: 100%;
|
||||
}
|
||||
.subtitle {
|
||||
|
|
|
|||
|
|
@ -25,12 +25,39 @@ body {
|
|||
}
|
||||
|
||||
.content {
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Media query for phones in portrait mode (max-width: 767px) */
|
||||
@media (max-width: 767px) {
|
||||
.content {
|
||||
max-width: 100%; /* Full width for small screens */
|
||||
}
|
||||
}
|
||||
|
||||
/* Media query for tablets (min-width: 768px) */
|
||||
@media (min-width: 768px) {
|
||||
.content {
|
||||
max-width: 750px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Media query for small desktops (min-width: 992px) */
|
||||
@media (min-width: 992px) {
|
||||
.content {
|
||||
max-width: 970px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Media query for large desktops (min-width: 1200px) */
|
||||
@media (min-width: 1800px) {
|
||||
.content {
|
||||
max-width: 1770px;
|
||||
}
|
||||
}
|
||||
|
||||
.app {
|
||||
|
|
@ -55,159 +82,3 @@ main {
|
|||
padding: 2rem 2rem;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
|
||||
main {
|
||||
height: 85%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 5rem;
|
||||
}
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app {
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
left: 0.5rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: xx-large;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.text-base {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.center-v-align {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.center-v-align > * {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.center-h-align {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.center-h-align > * {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.end-h-align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36pt;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.quit-btn {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mt-1\/2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.mb-1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.mb-5 {
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: #5271ff;
|
||||
}
|
||||
|
||||
.w-12 {
|
||||
width: 13rem;
|
||||
} */
|
||||
|
|
@ -370,7 +370,7 @@ const Dashboard: React.FC = () => {
|
|||
<TextField
|
||||
onChange={handleSearch}
|
||||
value={searchTerm}
|
||||
placeholder="Rechercher un quiz"
|
||||
placeholder="Rechercher un quiz par son titre"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// EditorQuiz.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { FolderType } from '../../../Types/FolderType';
|
||||
|
|
@ -11,11 +11,12 @@ import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePr
|
|||
import { QuizType } from '../../../Types/QuizType';
|
||||
|
||||
import './editorQuiz.css';
|
||||
import { Button, TextField, NativeSelect, IconButton } from '@mui/material';
|
||||
import { Send } from '@mui/icons-material';
|
||||
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
|
||||
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
|
||||
|
||||
import ApiService from '../../../services/ApiService';
|
||||
import { escapeForGIFT } from '../../../utils/giftUtils';
|
||||
import { Upload } from '@mui/icons-material';
|
||||
|
||||
interface EditQuizParams {
|
||||
id: string;
|
||||
|
|
@ -37,6 +38,8 @@ const QuizForm: React.FC = () => {
|
|||
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedFolder(event.target.value);
|
||||
};
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
|
|
@ -135,8 +138,13 @@ const QuizForm: React.FC = () => {
|
|||
try {
|
||||
const inputElement = document.getElementById('file-input') as HTMLInputElement;
|
||||
|
||||
if (!inputElement?.files || inputElement.files.length === 0) {
|
||||
setDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inputElement.files || inputElement.files.length === 0) {
|
||||
window.alert("Veuillez d'abord choisir un fichier à télécharger")
|
||||
window.alert("Veuillez d'abord choisir une image à téléverser.")
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +157,11 @@ const QuizForm: React.FC = () => {
|
|||
}
|
||||
|
||||
setImageLinks(prevLinks => [...prevLinks, imageUrl]);
|
||||
|
||||
// Reset the file input element
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||
}
|
||||
|
|
@ -172,73 +185,103 @@ const QuizForm: React.FC = () => {
|
|||
<div className='dumb'></div>
|
||||
</div>
|
||||
|
||||
<div className='editSection'>
|
||||
{/* <h2 className="subtitle">Éditeur</h2> */}
|
||||
|
||||
<div className='edit'>
|
||||
<h2 className="subtitle">Éditeur</h2>
|
||||
<TextField
|
||||
onChange={handleQuizTitleChange}
|
||||
value={quizTitle}
|
||||
placeholder="Titre du quiz"
|
||||
label="Titre du quiz"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<label>Choisir un dossier:
|
||||
<NativeSelect
|
||||
id="select-folder"
|
||||
color="primary"
|
||||
value={selectedFolder}
|
||||
onChange={handleSelectFolder}
|
||||
disabled={!isNewQuiz}
|
||||
style={{ marginBottom: '16px' }} // Ajout de marge en bas
|
||||
>
|
||||
<option disabled value=""> Choisir un dossier... </option>
|
||||
|
||||
{folders.map((folder: FolderType) => (
|
||||
<option value={folder._id} key={folder._id}> {folder.title} </option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
|
||||
<Editor initialValue={value} onEditorChange={handleUpdatePreview} />
|
||||
|
||||
<div className='images'>
|
||||
<div className='upload'>
|
||||
<label className="dropArea">
|
||||
<input type="file" id="file-input" className="file-input" accept="image/jpeg" multiple />
|
||||
</label>
|
||||
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleSaveImage}
|
||||
> <Send /> </IconButton>
|
||||
|
||||
</div>
|
||||
|
||||
<h2 className="subtitle">Mes images :</h2>
|
||||
|
||||
<div>
|
||||
<ul>
|
||||
{imageLinks.map((link, index) => (
|
||||
<li key={index}>
|
||||
<code
|
||||
onClick={() => handleCopyToClipboard(`<img ${link} >`)}>
|
||||
{`<img ${link} >`}
|
||||
</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</NativeSelect></label>
|
||||
|
||||
<Button variant="contained" onClick={handleQuizSave}>
|
||||
Enregistrer
|
||||
</Button>
|
||||
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
|
||||
<div className='editSection'>
|
||||
|
||||
<div className='edit'>
|
||||
<Editor
|
||||
label="Contenu GIFT du quiz:"
|
||||
initialValue={value}
|
||||
onEditorChange={handleUpdatePreview} />
|
||||
|
||||
<div className='images'>
|
||||
<div className='upload'>
|
||||
<label className="dropArea">
|
||||
<input type="file" id="file-input" className="file-input"
|
||||
accept="image/jpeg, image/png"
|
||||
multiple
|
||||
ref={fileInputRef} />
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-label='Téléverser'
|
||||
onClick={handleSaveImage}>
|
||||
Téléverser <Upload />
|
||||
</Button>
|
||||
|
||||
</label>
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)} >
|
||||
<DialogTitle>Erreur</DialogTitle>
|
||||
<DialogContent>
|
||||
Veuillez d'abord choisir une image à téléverser.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(false)} color="primary">
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<h4>Mes images :</h4>
|
||||
|
||||
<div>
|
||||
<div>(Cliquez sur un lien pour le copier)</div>
|
||||
<ul>
|
||||
{imageLinks.map((link, index) => {
|
||||
const imgTag = `} "texte de l'infobulle")`;
|
||||
return (
|
||||
<li key={index}>
|
||||
<code
|
||||
onClick={() => handleCopyToClipboard(imgTag)}>
|
||||
{imgTag}
|
||||
</code>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GiftCheatSheet />
|
||||
|
||||
</div>
|
||||
|
||||
<div className='preview'>
|
||||
<div className="preview-column">
|
||||
<h2 className="subtitle">Prévisualisation</h2>
|
||||
<h4>Prévisualisation</h4>
|
||||
<div>
|
||||
<GIFTTemplatePreview questions={filteredValue} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,10 +44,20 @@
|
|||
.quizEditor .editSection .edit .upload {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
align-items: right;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.upload .dropArea {
|
||||
flex-direction: column;
|
||||
/* align-items: stretch; */
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
|
|||
4
client/src/utils/giftUtils.ts
Normal file
4
client/src/utils/giftUtils.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export function escapeForGIFT(link: string): string {
|
||||
const specialChars = /[{}#~=<>\:]/g;
|
||||
return link.replace(specialChars, (match) => `\\${match}`);
|
||||
}
|
||||
Loading…
Reference in a new issue