feat: replace quill with the tiptap editor (#400)
* feat: initial tiptap * feat: initial tiptap * chore: styling * feat: add link * chore: add editor styling * feat: add password injection button * fix: make the editor work on the secret route * feat: add read only menu * chore: remove test content * fix: remove quill * fix: set read only after secret is created * fix: ensure read only * chore: update the height * chore: set correct colros for the title bg * chore: use generate password dep * feat: enable all password features * fix: clear the editor on create new * feat: use translation * feat: add languages * chore: remove unused file * fix: replace with useeffect * chore: pass extra props
This commit is contained in:
parent
f8740e37b0
commit
c75fef10c2
736
client/components/editor/index.jsx
Normal file
736
client/components/editor/index.jsx
Normal file
@ -0,0 +1,736 @@
|
||||
import {
|
||||
IconArrowBackUp,
|
||||
IconArrowForwardUp,
|
||||
IconBinary,
|
||||
IconBold,
|
||||
IconBrandCodesandbox,
|
||||
IconCode,
|
||||
IconCopy,
|
||||
IconH1,
|
||||
IconH2,
|
||||
IconH3,
|
||||
IconItalic,
|
||||
IconKey,
|
||||
IconLetterP,
|
||||
IconLink,
|
||||
IconLinkOff,
|
||||
IconList,
|
||||
IconListNumbers,
|
||||
IconQuote,
|
||||
IconStrikethrough,
|
||||
IconTextCaption,
|
||||
IconX,
|
||||
} from '@tabler/icons';
|
||||
import { Color } from '@tiptap/extension-color';
|
||||
import Link from '@tiptap/extension-link';
|
||||
import ListItem from '@tiptap/extension-list-item';
|
||||
import TextStyle from '@tiptap/extension-text-style';
|
||||
import { EditorProvider, useCurrentEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { generate } from 'generate-password-browser';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const generatePassword = (
|
||||
length = 16,
|
||||
options = { numbers: true, symbols: true, uppercase: true, lowercase: true }
|
||||
) => {
|
||||
const password = generate({
|
||||
length,
|
||||
numbers: options.numbers,
|
||||
symbols: options.symbols,
|
||||
uppercase: options.uppercase,
|
||||
lowercase: options.lowercase,
|
||||
});
|
||||
|
||||
return password;
|
||||
};
|
||||
|
||||
// Tooltip component for buttons
|
||||
const Tooltip = ({ text, children }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block">
|
||||
<div onMouseEnter={() => setIsVisible(true)} onMouseLeave={() => setIsVisible(false)}>
|
||||
{children}
|
||||
</div>
|
||||
{isVisible && (
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 text-xs font-medium text-white bg-gray-800 rounded shadow-sm whitespace-nowrap z-10">
|
||||
{text}
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-800"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Link Modal Component
|
||||
const LinkModal = ({ isOpen, onClose, onSubmit, initialUrl = '' }) => {
|
||||
const [url, setUrl] = useState(initialUrl);
|
||||
const inputRef = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Focus the input when the modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus();
|
||||
}, 50);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(url);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-800 rounded-lg shadow-lg p-6 w-full max-w-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-100">
|
||||
{t('editor.link_modal.title')}
|
||||
</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-200">
|
||||
<IconX size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="url"
|
||||
className="block text-sm font-medium text-gray-300 mb-2"
|
||||
>
|
||||
{t('editor.link_modal.url_label')}
|
||||
</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
id="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder={t('editor.link_modal.url_placeholder')}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-gray-700 text-gray-200 rounded-md hover:bg-gray-600"
|
||||
>
|
||||
{t('editor.link_modal.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-primary text-white rounded-md "
|
||||
>
|
||||
{initialUrl
|
||||
? t('editor.link_modal.update')
|
||||
: t('editor.link_modal.insert')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Password Modal Component
|
||||
const PasswordModal = ({ isOpen, onClose, onSubmit }) => {
|
||||
const [passwordLength, setPasswordLength] = useState(16);
|
||||
const [password, setPassword] = useState(generatePassword(16));
|
||||
const [options, setOptions] = useState({
|
||||
numbers: true,
|
||||
symbols: true,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const regeneratePassword = () => {
|
||||
setPassword(generatePassword(passwordLength, options));
|
||||
};
|
||||
|
||||
const handleOptionChange = (option) => {
|
||||
// Prevent disabling all options - at least one must be enabled
|
||||
const newOptions = { ...options, [option]: !options[option] };
|
||||
if (Object.values(newOptions).some((value) => value)) {
|
||||
setOptions(newOptions);
|
||||
setPassword(generatePassword(passwordLength, newOptions));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(password);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-800 rounded-lg shadow-lg p-6 w-full max-w-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-100">
|
||||
{t('editor.password_modal.title')}
|
||||
</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-200">
|
||||
<IconX size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t('editor.password_modal.length_label')}
|
||||
</label>
|
||||
<div className="flex items-center mb-4">
|
||||
<input
|
||||
type="range"
|
||||
min="8"
|
||||
max="32"
|
||||
value={passwordLength}
|
||||
onChange={(e) => {
|
||||
const newLength = parseInt(e.target.value);
|
||||
setPasswordLength(newLength);
|
||||
setPassword(generatePassword(newLength, options));
|
||||
}}
|
||||
className="w-full mr-3 accent-primary"
|
||||
style={{
|
||||
// Fallback for browsers that don't support accent-color
|
||||
'--tw-accent-color': 'var(--color-primary)',
|
||||
}}
|
||||
/>
|
||||
<span className="text-gray-200 w-8 text-center">{passwordLength}</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t('editor.password_modal.options_label')}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="numbers"
|
||||
checked={options.numbers}
|
||||
onChange={() => handleOptionChange('numbers')}
|
||||
className="mr-2 checked:bg-primary checked:hover:bg-primary/80 checked:focus:bg-primary/60 checked:active:bg-primary/60"
|
||||
/>
|
||||
<label htmlFor="numbers" className="text-gray-300 text-sm">
|
||||
{t('editor.password_modal.include_numbers')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="symbols"
|
||||
checked={options.symbols}
|
||||
onChange={() => handleOptionChange('symbols')}
|
||||
className="mr-2 checked:bg-primary checked:hover:bg-primary/80 checked:active:bg-primary/60 checked:focus:bg-primary/60"
|
||||
/>
|
||||
<label htmlFor="symbols" className="text-gray-300 text-sm">
|
||||
{t('editor.password_modal.include_symbols')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="uppercase"
|
||||
checked={options.uppercase}
|
||||
onChange={() => handleOptionChange('uppercase')}
|
||||
className="mr-2 checked:bg-primary checked:hover:bg-primary/80 checked:active:bg-primary/60 checked:focus:bg-primary/60"
|
||||
/>
|
||||
<label htmlFor="uppercase" className="text-gray-300 text-sm">
|
||||
{t('editor.password_modal.include_uppercase')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="lowercase"
|
||||
checked={options.lowercase}
|
||||
onChange={() => handleOptionChange('lowercase')}
|
||||
className="mr-2 checked:bg-primary checked:hover:bg-primary/80 checked:active:bg-primary/60 checked:focus:bg-primary/60"
|
||||
/>
|
||||
<label htmlFor="lowercase" className="text-gray-300 text-sm">
|
||||
{t('editor.password_modal.include_lowercase')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t('editor.password_modal.generated_password')}
|
||||
</label>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={password}
|
||||
readOnly
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-600 rounded-l-md text-gray-100"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={regeneratePassword}
|
||||
className="px-3 py-2 bg-primary text-gray-200 rounded-r-md hover:opacity-90"
|
||||
>
|
||||
{t('editor.password_modal.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-gray-700 text-gray-200 rounded-md hover:bg-gray-600"
|
||||
>
|
||||
{t('editor.password_modal.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-primary text-white rounded-md hover:opacity-90"
|
||||
>
|
||||
{t('editor.password_modal.insert')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ReadOnlyMenuBar component for non-editable mode
|
||||
const ReadOnlyMenuBar = () => {
|
||||
const { editor } = useCurrentEditor();
|
||||
const [copySuccess, setCopySuccess] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const copyAsHTML = () => {
|
||||
const html = editor.getHTML();
|
||||
navigator.clipboard
|
||||
.writeText(html)
|
||||
.then(() => {
|
||||
setCopySuccess(t('editor.copy_success.html'));
|
||||
setTimeout(() => setCopySuccess(''), 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
};
|
||||
|
||||
const copyAsPlainText = () => {
|
||||
const text = editor.getText();
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
setCopySuccess(t('editor.copy_success.text'));
|
||||
setTimeout(() => setCopySuccess(''), 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
};
|
||||
|
||||
const copyAsBase64 = () => {
|
||||
const text = editor.getText();
|
||||
// Convert to Base64
|
||||
const base64Content = btoa(
|
||||
new TextEncoder()
|
||||
.encode(text)
|
||||
.reduce((acc, byte) => acc + String.fromCharCode(byte), '')
|
||||
);
|
||||
navigator.clipboard
|
||||
.writeText(base64Content)
|
||||
.then(() => {
|
||||
setCopySuccess(t('editor.copy_success.base64'));
|
||||
setTimeout(() => setCopySuccess(''), 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
};
|
||||
|
||||
const buttonClass =
|
||||
'p-1.5 text-sm rounded-md hover:bg-gray-700 text-gray-200 transition-colors';
|
||||
const groupClass = 'flex items-center border border-gray-700 rounded-md bg-gray-800 shadow-sm';
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex w-full">
|
||||
<div className="flex gap-2">
|
||||
<div className={groupClass}>
|
||||
<Tooltip text={t('editor.tooltips.copy_html')}>
|
||||
<button onClick={copyAsHTML} className={buttonClass}>
|
||||
<IconCopy size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.copy_text')}>
|
||||
<button onClick={copyAsPlainText} className={buttonClass}>
|
||||
<IconTextCaption size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.copy_base64')}>
|
||||
<button onClick={copyAsBase64} className={buttonClass}>
|
||||
<IconBinary size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{copySuccess && (
|
||||
<div className="text-sm text-green-400 animate-fade-in-out">{copySuccess}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuBar = ({ content }) => {
|
||||
const { editor } = useCurrentEditor();
|
||||
const [linkModalOpen, setLinkModalOpen] = useState(false);
|
||||
const [passwordModalOpen, setPasswordModalOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the content is empty, clear the editor content
|
||||
// this is a hack until I figure out how to handle this better
|
||||
useEffect(() => {
|
||||
if (content === '') {
|
||||
editor.commands.clearContent();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Updated button styles without dark mode prefixes
|
||||
const buttonClass =
|
||||
'p-1.5 text-sm rounded-md hover:bg-gray-700 text-gray-200 transition-colors';
|
||||
const activeButtonClass =
|
||||
'p-1.5 text-sm rounded-md bg-blue-900/30 text-blue-400 transition-colors';
|
||||
|
||||
// Updated group styles without dark mode prefixes
|
||||
const groupClass = 'flex items-center border border-gray-700 rounded-md bg-gray-800 shadow-sm';
|
||||
|
||||
// Updated link handling function
|
||||
const openLinkModal = useCallback(() => {
|
||||
const previousUrl = editor.getAttributes('link').href || '';
|
||||
setLinkModalOpen(true);
|
||||
}, [editor]);
|
||||
|
||||
const handleLinkSubmit = useCallback(
|
||||
(url) => {
|
||||
// Empty
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add protocol if missing
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
url = 'https://' + url;
|
||||
}
|
||||
|
||||
// Update link
|
||||
try {
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
// Password handling function
|
||||
const handlePasswordSubmit = useCallback(
|
||||
(password) => {
|
||||
editor.chain().focus().insertContent(password).run();
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{/* Text formatting group */}
|
||||
<div className={groupClass}>
|
||||
<Tooltip text={t('editor.tooltips.bold')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
disabled={!editor.can().chain().focus().toggleBold().run()}
|
||||
className={
|
||||
editor.isActive('bold') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconBold size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.italic')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
disabled={!editor.can().chain().focus().toggleItalic().run()}
|
||||
className={
|
||||
editor.isActive('italic') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconItalic size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.strikethrough')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
disabled={!editor.can().chain().focus().toggleStrike().run()}
|
||||
className={
|
||||
editor.isActive('strike') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconStrikethrough
|
||||
size={18}
|
||||
stroke={1.5}
|
||||
className="text-gray-300"
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.inline_code')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
disabled={!editor.can().chain().focus().toggleCode().run()}
|
||||
className={
|
||||
editor.isActive('code') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconCode size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.link')}>
|
||||
<button
|
||||
onClick={openLinkModal}
|
||||
className={
|
||||
editor.isActive('link') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconLink size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.remove_link')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().unsetLink().run()}
|
||||
disabled={!editor.isActive('link')}
|
||||
className={`${buttonClass} disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
>
|
||||
<IconLinkOff size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.insert_password')}>
|
||||
<button
|
||||
onClick={() => setPasswordModalOpen(true)}
|
||||
className={buttonClass}
|
||||
>
|
||||
<IconKey size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Paragraph formatting group */}
|
||||
<div className={groupClass}>
|
||||
<Tooltip text={t('editor.tooltips.paragraph')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||
className={
|
||||
editor.isActive('paragraph') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconLetterP size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.heading1')}>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||
}
|
||||
className={
|
||||
editor.isActive('heading', { level: 1 })
|
||||
? activeButtonClass
|
||||
: buttonClass
|
||||
}
|
||||
>
|
||||
<IconH1 size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.heading2')}>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
}
|
||||
className={
|
||||
editor.isActive('heading', { level: 2 })
|
||||
? activeButtonClass
|
||||
: buttonClass
|
||||
}
|
||||
>
|
||||
<IconH2 size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.heading3')}>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
}
|
||||
className={
|
||||
editor.isActive('heading', { level: 3 })
|
||||
? activeButtonClass
|
||||
: buttonClass
|
||||
}
|
||||
>
|
||||
<IconH3 size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* List formatting group */}
|
||||
<div className={groupClass}>
|
||||
<Tooltip text={t('editor.tooltips.bullet_list')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
className={
|
||||
editor.isActive('bulletList') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconList size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.numbered_list')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
className={
|
||||
editor.isActive('orderedList') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconListNumbers size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.blockquote')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
className={
|
||||
editor.isActive('blockquote') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconQuote size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.code_block')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
className={
|
||||
editor.isActive('codeBlock') ? activeButtonClass : buttonClass
|
||||
}
|
||||
>
|
||||
<IconBrandCodesandbox
|
||||
size={18}
|
||||
stroke={1.5}
|
||||
className="text-gray-300"
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* History controls */}
|
||||
<div className={groupClass}>
|
||||
<Tooltip text={t('editor.tooltips.undo')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editor.can().chain().focus().undo().run()}
|
||||
className={`${buttonClass} disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
>
|
||||
<IconArrowBackUp size={18} stroke={1.5} className="text-gray-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text={t('editor.tooltips.redo')}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editor.can().chain().focus().redo().run()}
|
||||
className={`${buttonClass} disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
>
|
||||
<IconArrowForwardUp
|
||||
size={18}
|
||||
stroke={1.5}
|
||||
className="text-gray-300"
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Link Modal */}
|
||||
<LinkModal
|
||||
isOpen={linkModalOpen}
|
||||
onClose={() => setLinkModalOpen(false)}
|
||||
onSubmit={handleLinkSubmit}
|
||||
initialUrl={editor?.getAttributes('link').href || ''}
|
||||
/>
|
||||
|
||||
{/* Password Modal */}
|
||||
<PasswordModal
|
||||
isOpen={passwordModalOpen}
|
||||
onClose={() => setPasswordModalOpen(false)}
|
||||
onSubmit={handlePasswordSubmit}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const extensions = [
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
TextStyle.configure({ types: [ListItem.name] }),
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
autolink: true,
|
||||
defaultProtocol: 'https',
|
||||
protocols: ['http', 'https'],
|
||||
validate: (href) => /^https?:\/\//.test(href),
|
||||
}),
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
||||
},
|
||||
orderedList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export default function Editor({ content = '', setContent, editable = true, ...props }) {
|
||||
return (
|
||||
<div className="prose prose-sm max-w-none border border-gray-700 rounded-md p-2 h-full flex flex-col">
|
||||
<EditorProvider
|
||||
slotBefore={editable ? <MenuBar content={content} /> : <ReadOnlyMenuBar />}
|
||||
extensions={extensions}
|
||||
editable={editable}
|
||||
content={content}
|
||||
onUpdate={({ editor }) => {
|
||||
if (setContent) {
|
||||
setContent(editor.getHTML());
|
||||
}
|
||||
}}
|
||||
editorProps={{
|
||||
attributes: {
|
||||
class: 'flex-1 overflow-auto focus:outline-none text-gray-100 prose-headings:mt-6 prose-headings:first:mt-0 prose-headings:text-gray-100 prose-h1:text-2xl prose-h1:font-bold prose-h1:mb-4 prose-h2:text-xl prose-h2:font-bold prose-h2:mb-3 prose-h3:text-lg prose-h3:font-semibold prose-h3:mb-3 prose-p:my-3 prose-p:leading-relaxed prose-p:text-gray-200 prose-strong:text-gray-200 prose-strong:font-bold prose-em:text-gray-200 prose-ul:pl-5 prose-ul:my-3 prose-ol:pl-5 prose-ol:my-3 prose-li:my-1 prose-li:leading-normal prose-li:text-gray-200 prose-a:text-gray-100 prose-a:underline prose-a:font-medium hover:prose-a:text-gray-50 prose-code:bg-gray-800 prose-code:text-gray-200 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono prose-pre:bg-gray-900 prose-pre:text-white prose-pre:p-4 prose-pre:rounded-lg prose-pre:my-4 prose-pre:overflow-auto prose-pre:code:bg-transparent prose-pre:code:p-0 prose-pre:code:text-sm prose-pre:code:font-mono prose-blockquote:border-l-4 prose-blockquote:border-gray-600 prose-blockquote:pl-4 prose-blockquote:py-1 prose-blockquote:my-4 prose-blockquote:italic prose-blockquote:text-gray-300 prose-hr:my-6 prose-hr:border-gray-700',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
import { IconKey } from '@tabler/icons';
|
||||
import { generate } from 'generate-password-browser';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactQuill from 'react-quill';
|
||||
import 'react-quill/dist/quill.snow.css';
|
||||
|
||||
// https://github.com/zenoamaro/react-quill/issues/836#issuecomment-2440290893
|
||||
class ReactQuillFixed extends ReactQuill {
|
||||
destroyEditor() {
|
||||
super.destroyEditor();
|
||||
delete this.editor;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom button component for password generation
|
||||
const PasswordButton = ({ onClick, tooltip }) => (
|
||||
<button
|
||||
type="button"
|
||||
className="ql-customButton p-1 hover:bg-gray-700 rounded-md transition-colors"
|
||||
onClick={onClick}
|
||||
title={tooltip}
|
||||
>
|
||||
<IconKey size={18} className="text-gray-400" />
|
||||
</button>
|
||||
);
|
||||
|
||||
const Quill = ({ value, onChange, readOnly, defaultValue }) => {
|
||||
const quillRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(event.target) &&
|
||||
quillRef.current
|
||||
) {
|
||||
const editor = quillRef.current.getEditor();
|
||||
if (editor) {
|
||||
editor.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Custom handler for password generation
|
||||
const handlePasswordGeneration = () => {
|
||||
if (!quillRef.current) return;
|
||||
|
||||
const editor = quillRef.current.getEditor();
|
||||
if (!editor) return;
|
||||
|
||||
const password = generate({
|
||||
length: 16,
|
||||
numbers: true,
|
||||
symbols: true,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
});
|
||||
const range = editor.getSelection(true);
|
||||
editor.insertText(range.index, password + '\n');
|
||||
};
|
||||
|
||||
// Define modules based on readOnly state
|
||||
const modules = readOnly
|
||||
? {
|
||||
toolbar: false, // Disable toolbar in read-only mode
|
||||
}
|
||||
: {
|
||||
toolbar: {
|
||||
container: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['link', 'code-block'],
|
||||
['clean'],
|
||||
// Custom button container
|
||||
[{ 'custom-button': 'password' }],
|
||||
],
|
||||
handlers: {
|
||||
'custom-button': handlePasswordGeneration,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="bg-gray-800 border border-gray-700 rounded-md overflow-hidden"
|
||||
>
|
||||
<div className="relative">
|
||||
<ReactQuillFixed
|
||||
ref={quillRef}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
placeholder={defaultValue}
|
||||
theme={readOnly ? 'bubble' : 'snow'}
|
||||
modules={modules}
|
||||
className="bg-gray-800 text-gray-100 placeholder-gray-500 [&_.ql-editor]:min-h-[200px] [&_.ql-editor]:text-base [&_.ql-editor]:font-sans [&_.ql-editor]:leading-relaxed
|
||||
[&_.ql-toolbar]:border-gray-700 [&_.ql-toolbar]:bg-gray-900
|
||||
[&_.ql-container]:border-gray-700
|
||||
[&_.ql-editor_h1]:text-3xl [&_.ql-editor_h1]:font-bold [&_.ql-editor_h1]:mb-4
|
||||
[&_.ql-editor_h2]:text-2xl [&_.ql-editor_h2]:font-bold [&_.ql-editor_h2]:mb-3
|
||||
[&_.ql-editor_h3]:text-xl [&_.ql-editor_h3]:font-bold [&_.ql-editor_h3]:mb-2
|
||||
[&_.ql-editor_p]:mb-4
|
||||
[&_.ql-editor_ul]:list-disc [&_.ql-editor_ul]:ml-4
|
||||
[&_.ql-editor_ol]:list-decimal [&_.ql-editor_ol]:ml-4
|
||||
[&_.ql-snow_.ql-toolbar_button]:text-gray-300
|
||||
[&_.ql-snow_.ql-toolbar_button:hover]:text-white"
|
||||
/>
|
||||
{!readOnly && (
|
||||
<div className="absolute top-0 right-0 transform -translate-x-1/2 h-[42px] flex items-center">
|
||||
<div className="w-[1px] h-5 bg-gray-700 mx-4"></div>
|
||||
<PasswordButton
|
||||
onClick={handlePasswordGeneration}
|
||||
tooltip={t('home.inject_password')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Quill;
|
@ -1,4 +1,4 @@
|
||||
export function Switch({ checked, onChange, className = '', children }) {
|
||||
export function Switch({ checked, onChange, className = '', children, ...props }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@ -8,6 +8,7 @@ export function Switch({ checked, onChange, className = '', children }) {
|
||||
className={`${
|
||||
checked ? 'bg-primary' : 'bg-gray-700'
|
||||
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-gray-900 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
<span className="sr-only">{children}</span>
|
||||
<span
|
||||
|
105
client/index.css
105
client/index.css
@ -165,10 +165,6 @@ html {
|
||||
--margin-basic: 10px;
|
||||
}
|
||||
|
||||
.ql-editor {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@apply font-sans bg-gray-900 text-gray-100;
|
||||
@ -188,104 +184,3 @@ html {
|
||||
@apply btn bg-red-500 text-white hover:bg-red-600;
|
||||
}
|
||||
}
|
||||
|
||||
/* Quill Editor Styles */
|
||||
.ql-toolbar {
|
||||
border-color: rgb(55, 65, 81) !important; /* border-gray-700 */
|
||||
background-color: rgb(31, 41, 55) !important; /* bg-gray-800 */
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
border-color: rgb(55, 65, 81) !important; /* border-gray-700 */
|
||||
background-color: rgb(31, 41, 55) !important; /* bg-gray-800 */
|
||||
}
|
||||
|
||||
.ql-editor {
|
||||
color: rgb(243, 244, 246) !important; /* text-gray-100 */
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.ql-editor.ql-blank::before {
|
||||
color: rgb(107, 114, 128) !important; /* text-gray-500 */
|
||||
}
|
||||
|
||||
/* Toolbar buttons - Updated colors */
|
||||
.ql-snow.ql-toolbar button,
|
||||
.ql-snow .ql-toolbar button {
|
||||
color: rgb(209, 213, 219) !important; /* text-gray-300 */
|
||||
}
|
||||
|
||||
.ql-snow.ql-toolbar button:hover,
|
||||
.ql-snow .ql-toolbar button:hover {
|
||||
color: rgb(243, 244, 246) !important; /* text-gray-100 */
|
||||
}
|
||||
|
||||
.ql-snow.ql-toolbar button.ql-active,
|
||||
.ql-snow .ql-toolbar button.ql-active {
|
||||
color: rgb(255, 255, 255) !important; /* text-white */
|
||||
background-color: rgb(75, 85, 99) !important; /* bg-gray-600 */
|
||||
}
|
||||
|
||||
/* Toolbar icons - Updated colors */
|
||||
.ql-snow .ql-stroke {
|
||||
stroke: rgb(209, 213, 219) !important; /* text-gray-300 */
|
||||
}
|
||||
|
||||
.ql-snow.ql-toolbar button:hover .ql-stroke,
|
||||
.ql-snow .ql-toolbar button:hover .ql-stroke,
|
||||
.ql-snow.ql-toolbar button.ql-active .ql-stroke,
|
||||
.ql-snow .ql-toolbar button.ql-active .ql-stroke {
|
||||
stroke: rgb(255, 255, 255) !important; /* text-white */
|
||||
}
|
||||
|
||||
.ql-snow .ql-fill {
|
||||
fill: rgb(209, 213, 219) !important; /* text-gray-300 */
|
||||
}
|
||||
|
||||
.ql-snow.ql-toolbar button:hover .ql-fill,
|
||||
.ql-snow .ql-toolbar button.ql-active .ql-fill {
|
||||
fill: rgb(255, 255, 255) !important; /* text-white */
|
||||
}
|
||||
|
||||
/* Dropdown menus */
|
||||
.ql-snow .ql-picker {
|
||||
color: rgb(209, 213, 219) !important; /* text-gray-300 */
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-options {
|
||||
background-color: rgb(31, 41, 55) !important; /* bg-gray-800 */
|
||||
border-color: rgb(55, 65, 81) !important; /* border-gray-700 */
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-expanded .ql-picker-label,
|
||||
.ql-snow .ql-picker.ql-expanded .ql-picker-options {
|
||||
border-color: rgb(55, 65, 81) !important; /* border-gray-700 */
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-label:hover,
|
||||
.ql-snow .ql-picker-item:hover,
|
||||
.ql-snow .ql-picker-item.ql-selected {
|
||||
color: rgb(255, 255, 255) !important; /* text-white */
|
||||
}
|
||||
|
||||
/* Links in editor */
|
||||
.ql-editor a {
|
||||
color: rgb(209, 213, 219) !important; /* text-gray-300 */
|
||||
}
|
||||
|
||||
/* Quill Bubble Theme Styles for Read-Only Mode */
|
||||
.ql-bubble .ql-editor {
|
||||
padding: 1rem !important;
|
||||
min-height: 100px !important;
|
||||
background-color: rgb(31, 41, 55) !important; /* bg-gray-800 */
|
||||
color: rgb(243, 244, 246) !important; /* text-gray-100 */
|
||||
}
|
||||
|
||||
/* Remove default bubble theme tooltips */
|
||||
.ql-bubble .ql-tooltip {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-label.ql-active {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,6 @@ import {
|
||||
IconFile,
|
||||
IconHeading,
|
||||
IconLock,
|
||||
IconPerspective,
|
||||
IconShieldLock,
|
||||
IconSquarePlus,
|
||||
} from '@tabler/icons';
|
||||
@ -15,8 +14,8 @@ import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { decrypt } from '../../../shared/helpers/crypto';
|
||||
import { getSecret, secretExists } from '../../api/secret';
|
||||
import { downloadFile } from '../../api/upload';
|
||||
import Editor from '../../components/editor';
|
||||
import ErrorBox from '../../components/error-box';
|
||||
import Quill from '../../components/quill';
|
||||
|
||||
const getEncryptionKeyHash = (hash) => {
|
||||
const id = '#encryption_key=';
|
||||
@ -42,7 +41,6 @@ const Secret = () => {
|
||||
const [files, setFiles] = useState(null);
|
||||
const [isDownloaded, setIsDownloaded] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
const [hasConvertedBase64ToPlain, setHasConvertedBase64ToPlain] = useState(false);
|
||||
|
||||
// Fetch secret existence on mount
|
||||
useEffect(() => {
|
||||
@ -142,15 +140,6 @@ const Secret = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const convertBase64ToPlain = () => {
|
||||
if (!hasConvertedBase64ToPlain) {
|
||||
setSecret(btoa(secret));
|
||||
} else {
|
||||
setSecret(atob(secret));
|
||||
}
|
||||
setHasConvertedBase64ToPlain(!hasConvertedBase64ToPlain);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="space-y-6">
|
||||
@ -176,7 +165,7 @@ const Secret = () => {
|
||||
|
||||
{/* Main Content */}
|
||||
<div
|
||||
className={`space-y-6 p-6 rounded-lg transition-all duration-200 ${
|
||||
className={`space-y-6 md:p-6 rounded-lg transition-all duration-200 ${
|
||||
!isSecretOpen
|
||||
? 'bg-gray-800/30 border-2 border-gray-700/50 shadow-lg relative'
|
||||
: 'bg-gray-800/50'
|
||||
@ -268,7 +257,7 @@ const Secret = () => {
|
||||
|
||||
{/* Secret Content */}
|
||||
<div className="w-full">
|
||||
<Quill value={secret} secretId={secretId} readOnly />
|
||||
<Editor content={secret} editable={false} />
|
||||
</div>
|
||||
|
||||
{/* File Downloads Section */}
|
||||
@ -321,20 +310,6 @@ const Secret = () => {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Convert Base64 Button */}
|
||||
{isSecretOpen && (
|
||||
<button
|
||||
onClick={convertBase64ToPlain}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-800 text-gray-300
|
||||
hover:bg-gray-700 hover:text-white rounded-md transition-colors"
|
||||
>
|
||||
<IconPerspective size={14} />
|
||||
{!hasConvertedBase64ToPlain
|
||||
? t('secret.convert_b64')
|
||||
: t('secret.convert_utf8')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Create New Secret Button */}
|
||||
{isSecretOpen && (
|
||||
<Link
|
||||
|
1350
package-lock.json
generated
1350
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -44,7 +44,6 @@
|
||||
"@fastify/vite": "^7.0.1",
|
||||
"@prisma/client": "^4.12.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"config": "^3.3.6",
|
||||
"email-validator": "^2.0.4",
|
||||
@ -62,18 +61,22 @@
|
||||
"pretty-bytes": "^4.0.2",
|
||||
"recharts": "^2.13.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"unfetch": "^4.2.0",
|
||||
"validator": "^13.7.0",
|
||||
"y8": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"react-qr-code": "^2.0.8",
|
||||
"@tabler/icons": "^1.83.1",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"tweetnacl": "^0.14.5",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"react-router-dom": "^6.21.3",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tiptap/extension-color": "^2.11.5",
|
||||
"@tiptap/extension-link": "^2.11.5",
|
||||
"@tiptap/extension-list-item": "^2.11.5",
|
||||
"@tiptap/extension-text-style": "^2.11.5",
|
||||
"@tiptap/react": "^2.11.5",
|
||||
"@tiptap/starter-kit": "^2.11.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"buffer": "^6.0.3",
|
||||
"classcat": "^5.0.3",
|
||||
"dayjs": "^1.11.7",
|
||||
@ -95,7 +98,10 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-i18next": "^11.18.5",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-qr-code": "^2.0.8",
|
||||
"react-router-dom": "^6.21.3",
|
||||
"tweetnacl": "^0.14.5",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"vite": "^5.4.10",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
|
@ -274,5 +274,55 @@
|
||||
"not_logged_in": "Nejste přihlášeni",
|
||||
"something_went_wrong": "Něco se pokazilo!",
|
||||
"loading": "Načítání",
|
||||
"common": {}
|
||||
"common": {},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "Tučné",
|
||||
"italic": "Kurzíva",
|
||||
"strikethrough": "Přeškrtnuté",
|
||||
"inline_code": "Inline kód",
|
||||
"link": "Odkaz",
|
||||
"remove_link": "Odstranit odkaz",
|
||||
"insert_password": "Vložit heslo",
|
||||
"paragraph": "Odstavec",
|
||||
"heading1": "Nadpis 1",
|
||||
"heading2": "Nadpis 2",
|
||||
"heading3": "Nadpis 3",
|
||||
"bullet_list": "Odrážkový seznam",
|
||||
"numbered_list": "Číslovaný seznam",
|
||||
"blockquote": "Citace",
|
||||
"code_block": "Blok kódu",
|
||||
"undo": "Zpět",
|
||||
"redo": "Znovu",
|
||||
"copy_html": "Kopírovat jako HTML",
|
||||
"copy_text": "Kopírovat jako prostý text",
|
||||
"copy_base64": "Kopírovat jako Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "Vložit odkaz",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://priklad.cz",
|
||||
"cancel": "Zrušit",
|
||||
"insert": "Vložit",
|
||||
"update": "Aktualizovat"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "Vložit heslo",
|
||||
"length_label": "Délka hesla",
|
||||
"options_label": "Možnosti hesla",
|
||||
"include_numbers": "Zahrnout čísla",
|
||||
"include_symbols": "Zahrnout symboly",
|
||||
"include_uppercase": "Zahrnout velká písmena",
|
||||
"include_lowercase": "Zahrnout malá písmena",
|
||||
"generated_password": "Vygenerované heslo",
|
||||
"refresh": "Obnovit",
|
||||
"cancel": "Zrušit",
|
||||
"insert": "Vložit"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML zkopírováno!",
|
||||
"text": "Prostý text zkopírován!",
|
||||
"base64": "Base64 zkopírováno!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -259,5 +259,55 @@
|
||||
"password_protected": "Passwortgeschützt",
|
||||
"ip_restricted": "IP-beschränkt",
|
||||
"average_max_views": "Durchschnittlich erlaubte Aufrufe pro Geheimnis"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "Fett",
|
||||
"italic": "Kursiv",
|
||||
"strikethrough": "Durchgestrichen",
|
||||
"inline_code": "Inline-Code",
|
||||
"link": "Link",
|
||||
"remove_link": "Link entfernen",
|
||||
"insert_password": "Passwort einfügen",
|
||||
"paragraph": "Absatz",
|
||||
"heading1": "Überschrift 1",
|
||||
"heading2": "Überschrift 2",
|
||||
"heading3": "Überschrift 3",
|
||||
"bullet_list": "Aufzählungsliste",
|
||||
"numbered_list": "Nummerierte Liste",
|
||||
"blockquote": "Zitat",
|
||||
"code_block": "Codeblock",
|
||||
"undo": "Rückgängig",
|
||||
"redo": "Wiederherstellen",
|
||||
"copy_html": "Als HTML kopieren",
|
||||
"copy_text": "Als Klartext kopieren",
|
||||
"copy_base64": "Als Base64 kopieren"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "Link einfügen",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://beispiel.de",
|
||||
"cancel": "Abbrechen",
|
||||
"insert": "Einfügen",
|
||||
"update": "Aktualisieren"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "Passwort einfügen",
|
||||
"length_label": "Passwortlänge",
|
||||
"options_label": "Passwortoptionen",
|
||||
"include_numbers": "Zahlen einschließen",
|
||||
"include_symbols": "Symbole einschließen",
|
||||
"include_uppercase": "Großbuchstaben einschließen",
|
||||
"include_lowercase": "Kleinbuchstaben einschließen",
|
||||
"generated_password": "Generiertes Passwort",
|
||||
"refresh": "Aktualisieren",
|
||||
"cancel": "Abbrechen",
|
||||
"insert": "Einfügen"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML kopiert!",
|
||||
"text": "Klartext kopiert!",
|
||||
"base64": "Base64 kopiert!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -275,5 +275,55 @@
|
||||
"password_protected": "Password Protected",
|
||||
"ip_restricted": "IP Restricted",
|
||||
"average_max_views": "Average Max Views Per Secret"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "Bold",
|
||||
"italic": "Italic",
|
||||
"strikethrough": "Strikethrough",
|
||||
"inline_code": "Inline Code",
|
||||
"link": "Link",
|
||||
"remove_link": "Remove Link",
|
||||
"insert_password": "Insert Password",
|
||||
"paragraph": "Paragraph",
|
||||
"heading1": "Heading 1",
|
||||
"heading2": "Heading 2",
|
||||
"heading3": "Heading 3",
|
||||
"bullet_list": "Bullet List",
|
||||
"numbered_list": "Numbered List",
|
||||
"blockquote": "Blockquote",
|
||||
"code_block": "Code Block",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"copy_html": "Copy as HTML",
|
||||
"copy_text": "Copy as Plain Text",
|
||||
"copy_base64": "Copy as Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "Insert Link",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://example.com",
|
||||
"cancel": "Cancel",
|
||||
"insert": "Insert",
|
||||
"update": "Update"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "Insert Password",
|
||||
"length_label": "Password Length",
|
||||
"options_label": "Password Options",
|
||||
"include_numbers": "Include Numbers",
|
||||
"include_symbols": "Include Symbols",
|
||||
"include_uppercase": "Include Uppercase",
|
||||
"include_lowercase": "Include Lowercase",
|
||||
"generated_password": "Generated Password",
|
||||
"refresh": "Refresh",
|
||||
"cancel": "Cancel",
|
||||
"insert": "Insert"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML copied!",
|
||||
"text": "Plain text copied!",
|
||||
"base64": "Base64 copied!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -276,5 +276,55 @@
|
||||
"password_protected": "Protegidos con contraseña",
|
||||
"ip_restricted": "Restringidos por IP",
|
||||
"average_max_views": "Promedio de vistas máximas por secreto"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "Negrita",
|
||||
"italic": "Cursiva",
|
||||
"strikethrough": "Tachado",
|
||||
"inline_code": "Código en línea",
|
||||
"link": "Enlace",
|
||||
"remove_link": "Eliminar enlace",
|
||||
"insert_password": "Insertar contraseña",
|
||||
"paragraph": "Párrafo",
|
||||
"heading1": "Encabezado 1",
|
||||
"heading2": "Encabezado 2",
|
||||
"heading3": "Encabezado 3",
|
||||
"bullet_list": "Lista con viñetas",
|
||||
"numbered_list": "Lista numerada",
|
||||
"blockquote": "Cita",
|
||||
"code_block": "Bloque de código",
|
||||
"undo": "Deshacer",
|
||||
"redo": "Rehacer",
|
||||
"copy_html": "Copiar como HTML",
|
||||
"copy_text": "Copiar como texto plano",
|
||||
"copy_base64": "Copiar como Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "Insertar enlace",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://ejemplo.com",
|
||||
"cancel": "Cancelar",
|
||||
"insert": "Insertar",
|
||||
"update": "Actualizar"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "Insertar contraseña",
|
||||
"length_label": "Longitud de la contraseña",
|
||||
"options_label": "Opciones de contraseña",
|
||||
"include_numbers": "Incluir números",
|
||||
"include_symbols": "Incluir símbolos",
|
||||
"include_uppercase": "Incluir mayúsculas",
|
||||
"include_lowercase": "Incluir minúsculas",
|
||||
"generated_password": "Contraseña generada",
|
||||
"refresh": "Actualizar",
|
||||
"cancel": "Cancelar",
|
||||
"insert": "Insertar"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "¡HTML copiado!",
|
||||
"text": "¡Texto plano copiado!",
|
||||
"base64": "¡Base64 copiado!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"sign_out": "Se déconnecter",
|
||||
"sign_up": "S'enregistrer",
|
||||
"not_logged_in": "Non connecté",
|
||||
"something_went_wrong": "Quelque chose s'est mal passé !",
|
||||
"something_went_wrong": "Quelque chose s'est mal passé !",
|
||||
"loading": "Chargement",
|
||||
"common": {},
|
||||
"account": {
|
||||
@ -16,7 +16,7 @@
|
||||
"upload_files": "Téléversement de fichiers",
|
||||
"expiration": "Délai d'expiration de secret de 14 ou 28 jours",
|
||||
"secrets": "Lister et supprimer vos secrets",
|
||||
"more": "Merci d'avoir rejoint Hemmelig.app !",
|
||||
"more": "Merci d'avoir rejoint Hemmelig.app !",
|
||||
"features": "Fonctionnalités"
|
||||
},
|
||||
"account": {
|
||||
@ -24,7 +24,7 @@
|
||||
"can_not_update_profile": "Impossible de mettre à jour votre compte utilisateur",
|
||||
"can_not_delete": "Impossible de supprimer l'utilisateur",
|
||||
"delete_account": "Supprimer votre compte",
|
||||
"do_you_want_delete": "Êtes-vous sûr(e) de vouloir supprimer votre compte ?",
|
||||
"do_you_want_delete": "Êtes-vous sûr(e) de vouloir supprimer votre compte ?",
|
||||
"dont_delete_account": "Non, ne pas le supprimer",
|
||||
"email": "Email",
|
||||
"your_password": "Mot de passe actuel",
|
||||
@ -44,13 +44,13 @@
|
||||
"no": "Non",
|
||||
"delete_secret": "Supprimer ce secret",
|
||||
"dont_delete_secret": "Non, ne pas le supprimer",
|
||||
"do_you_want_delete": "Êtes-vous sûr(e) de vouloir supprimer ce secret ?"
|
||||
"do_you_want_delete": "Êtes-vous sûr(e) de vouloir supprimer ce secret ?"
|
||||
},
|
||||
"settings": {
|
||||
"read_only_mode": "Mode lecture seule",
|
||||
"readonly_only_for_non_admin": "L'instance Hemmelig doit-elle être en lecture seule pour les utilisateurs non administrateurs ou créateurs ?",
|
||||
"readonly_only_for_non_admin": "L'instance Hemmelig doit-elle être en lecture seule pour les utilisateurs non administrateurs ou créateurs ?",
|
||||
"disable_users": "Désactiver les utilisateurs",
|
||||
"disable_signin": "Désactiver la connexion des utilisateurs ?",
|
||||
"disable_signin": "Désactiver la connexion des utilisateurs ?",
|
||||
"disable_user_account_creation": "Désactiver la création de nouveaux comptes",
|
||||
"disable_user_account_creation_description": "Empêcher la création de compte par tout le monde. Un administrateur pourra toujours créer de nouveaux comptes utilisateurs",
|
||||
"hide_allowed_ip_input": "Désactiver les restrictions d'IPs",
|
||||
@ -69,7 +69,7 @@
|
||||
"creator": "Créateur",
|
||||
"user": "Utilisateur",
|
||||
"delete": "Suppression",
|
||||
"do_you_want_delete": "Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?",
|
||||
"do_you_want_delete": "Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?",
|
||||
"delete_user": "Supprimer cet utilisateur",
|
||||
"dont_delete_user": "Non, ne pas le supprimer",
|
||||
"have_to_be_admin": "Vous devez être un administrateur pour voir les utilisateurs"
|
||||
@ -87,14 +87,14 @@
|
||||
},
|
||||
"session": {
|
||||
"auth": "Authentification",
|
||||
"expire": "Votre session va expirer. Voulez-vous la renouveler ?",
|
||||
"expire": "Votre session va expirer. Voulez-vous la renouveler ?",
|
||||
"update": "Renouveler la session"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"app_subtitle": "Collez un mot de passe, un message confidentiel ou des données privées.",
|
||||
"welcome": "Assurez-vous que vos données sensibles restent chiffrées, sécurisées et confidentielles.",
|
||||
"bummer": "C'est dommage !",
|
||||
"bummer": "C'est dommage !",
|
||||
"maintxtarea": "Écrivez vos informations sensibles",
|
||||
"file_upload": "Téléverser des fichiers",
|
||||
"content_title": "Titre",
|
||||
@ -156,7 +156,7 @@
|
||||
"one_time_use": "Ce secret ne peut être vu qu'une seule fois et s'autodétruira après avoir été lu.",
|
||||
"norwegian_meaning": "signifie « secret » en norvégien",
|
||||
"copy": "Copier dans le presse-papiers",
|
||||
"copied": "Copié !",
|
||||
"copied": "Copié !",
|
||||
"create": "Créer",
|
||||
"sign_in": "Connexion",
|
||||
"complete_url": "URL secrète complète",
|
||||
@ -174,7 +174,7 @@
|
||||
"secret": {
|
||||
"view_your_secret": "Afficher votre secret",
|
||||
"will_show_once": "Par défaut, le secret ne sera affiché qu'une seule fois.",
|
||||
"views_left": "Affichages restants :",
|
||||
"views_left": "Affichages restants :",
|
||||
"password_required": "Un mot de passe est nécessaire pour ouvrir ce secret",
|
||||
"view_secret": "Voir le secret",
|
||||
"create_secret": "Créer un nouveau secret",
|
||||
@ -240,7 +240,7 @@
|
||||
},
|
||||
"not_found": {
|
||||
"title": "Page introuvable",
|
||||
"description": "Oups ! Il semblerait que ce secret se soit évaporé. Ne vous inquiétez pas toutefois, vous pouvez en créer un nouveau ou consulter la documentation.",
|
||||
"description": "Oups ! Il semblerait que ce secret se soit évaporé. Ne vous inquiétez pas toutefois, vous pouvez en créer un nouveau ou consulter la documentation.",
|
||||
"go_home": "Créer un nouveau secret",
|
||||
"view_docs": "Voir la doc de l'API"
|
||||
},
|
||||
@ -258,5 +258,55 @@
|
||||
"average_views": "Nombre moyen de vues",
|
||||
"views_per_secret": "Vues par secret",
|
||||
"description": "Un aperçu complet de l'utilisation de votre plateforme et de ses fonctionnalités de sécurité"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "Gras",
|
||||
"italic": "Italique",
|
||||
"strikethrough": "Barré",
|
||||
"inline_code": "Code en ligne",
|
||||
"link": "Lien",
|
||||
"remove_link": "Supprimer le lien",
|
||||
"insert_password": "Insérer un mot de passe",
|
||||
"paragraph": "Paragraphe",
|
||||
"heading1": "Titre 1",
|
||||
"heading2": "Titre 2",
|
||||
"heading3": "Titre 3",
|
||||
"bullet_list": "Liste à puces",
|
||||
"numbered_list": "Liste numérotée",
|
||||
"blockquote": "Citation",
|
||||
"code_block": "Bloc de code",
|
||||
"undo": "Annuler",
|
||||
"redo": "Rétablir",
|
||||
"copy_html": "Copier en HTML",
|
||||
"copy_text": "Copier en texte brut",
|
||||
"copy_base64": "Copier en Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "Insérer un lien",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://exemple.com",
|
||||
"cancel": "Annuler",
|
||||
"insert": "Insérer",
|
||||
"update": "Mettre à jour"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "Insérer un mot de passe",
|
||||
"length_label": "Longueur du mot de passe",
|
||||
"options_label": "Options du mot de passe",
|
||||
"include_numbers": "Inclure des chiffres",
|
||||
"include_symbols": "Inclure des symboles",
|
||||
"include_uppercase": "Inclure des majuscules",
|
||||
"include_lowercase": "Inclure des minuscules",
|
||||
"generated_password": "Mot de passe généré",
|
||||
"refresh": "Actualiser",
|
||||
"cancel": "Annuler",
|
||||
"insert": "Insérer"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML copié !",
|
||||
"text": "Texte brut copié !",
|
||||
"base64": "Base64 copié !"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -260,5 +260,55 @@
|
||||
"password_protected": "Protetti da password",
|
||||
"ip_restricted": "Limitati per IP",
|
||||
"average_max_views": "Media visualizzazioni massime per segreto"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "Grassetto",
|
||||
"italic": "Corsivo",
|
||||
"strikethrough": "Barrato",
|
||||
"inline_code": "Codice in linea",
|
||||
"link": "Collegamento",
|
||||
"remove_link": "Rimuovi collegamento",
|
||||
"insert_password": "Inserisci password",
|
||||
"paragraph": "Paragrafo",
|
||||
"heading1": "Titolo 1",
|
||||
"heading2": "Titolo 2",
|
||||
"heading3": "Titolo 3",
|
||||
"bullet_list": "Elenco puntato",
|
||||
"numbered_list": "Elenco numerato",
|
||||
"blockquote": "Citazione",
|
||||
"code_block": "Blocco di codice",
|
||||
"undo": "Annulla",
|
||||
"redo": "Ripristina",
|
||||
"copy_html": "Copia come HTML",
|
||||
"copy_text": "Copia come testo semplice",
|
||||
"copy_base64": "Copia come Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "Inserisci collegamento",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://esempio.it",
|
||||
"cancel": "Annulla",
|
||||
"insert": "Inserisci",
|
||||
"update": "Aggiorna"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "Inserisci password",
|
||||
"length_label": "Lunghezza password",
|
||||
"options_label": "Opzioni password",
|
||||
"include_numbers": "Includi numeri",
|
||||
"include_symbols": "Includi simboli",
|
||||
"include_uppercase": "Includi maiuscole",
|
||||
"include_lowercase": "Includi minuscole",
|
||||
"generated_password": "Password generata",
|
||||
"refresh": "Aggiorna",
|
||||
"cancel": "Annulla",
|
||||
"insert": "Inserisci"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML copiato!",
|
||||
"text": "Testo semplice copiato!",
|
||||
"base64": "Base64 copiato!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -275,5 +275,55 @@
|
||||
"password_protected": "Met wachtwoord",
|
||||
"ip_restricted": "Beperkt op IP",
|
||||
"average_max_views": "Gemiddelde voor max. aantal weergaven"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "Vet",
|
||||
"italic": "Cursief",
|
||||
"strikethrough": "Doorhalen",
|
||||
"inline_code": "Inline code",
|
||||
"link": "Link",
|
||||
"remove_link": "Link verwijderen",
|
||||
"insert_password": "Wachtwoord invoegen",
|
||||
"paragraph": "Paragraaf",
|
||||
"heading1": "Kop 1",
|
||||
"heading2": "Kop 2",
|
||||
"heading3": "Kop 3",
|
||||
"bullet_list": "Opsommingstekens",
|
||||
"numbered_list": "Genummerde lijst",
|
||||
"blockquote": "Citaat",
|
||||
"code_block": "Codeblok",
|
||||
"undo": "Ongedaan maken",
|
||||
"redo": "Opnieuw",
|
||||
"copy_html": "Kopiëren als HTML",
|
||||
"copy_text": "Kopiëren als platte tekst",
|
||||
"copy_base64": "Kopiëren als Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "Link invoegen",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://voorbeeld.nl",
|
||||
"cancel": "Annuleren",
|
||||
"insert": "Invoegen",
|
||||
"update": "Bijwerken"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "Wachtwoord invoegen",
|
||||
"length_label": "Wachtwoordlengte",
|
||||
"options_label": "Wachtwoordopties",
|
||||
"include_numbers": "Cijfers opnemen",
|
||||
"include_symbols": "Symbolen opnemen",
|
||||
"include_uppercase": "Hoofdletters opnemen",
|
||||
"include_lowercase": "Kleine letters opnemen",
|
||||
"generated_password": "Gegenereerd wachtwoord",
|
||||
"refresh": "Vernieuwen",
|
||||
"cancel": "Annuleren",
|
||||
"insert": "Invoegen"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML gekopieerd!",
|
||||
"text": "Platte tekst gekopieerd!",
|
||||
"base64": "Base64 gekopieerd!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -264,5 +264,55 @@
|
||||
"password_protected": "Protegidos por palavra-passe",
|
||||
"ip_restricted": "Restritos por IP",
|
||||
"average_max_views": "Média de visualizações máximas por segredo"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "Negrito",
|
||||
"italic": "Itálico",
|
||||
"strikethrough": "Riscado",
|
||||
"inline_code": "Código em linha",
|
||||
"link": "Ligação",
|
||||
"remove_link": "Remover ligação",
|
||||
"insert_password": "Inserir palavra-passe",
|
||||
"paragraph": "Parágrafo",
|
||||
"heading1": "Título 1",
|
||||
"heading2": "Título 2",
|
||||
"heading3": "Título 3",
|
||||
"bullet_list": "Lista com marcadores",
|
||||
"numbered_list": "Lista numerada",
|
||||
"blockquote": "Citação",
|
||||
"code_block": "Bloco de código",
|
||||
"undo": "Desfazer",
|
||||
"redo": "Refazer",
|
||||
"copy_html": "Copiar como HTML",
|
||||
"copy_text": "Copiar como texto simples",
|
||||
"copy_base64": "Copiar como Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "Inserir ligação",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://exemplo.pt",
|
||||
"cancel": "Cancelar",
|
||||
"insert": "Inserir",
|
||||
"update": "Atualizar"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "Inserir palavra-passe",
|
||||
"length_label": "Comprimento da palavra-passe",
|
||||
"options_label": "Opções de palavra-passe",
|
||||
"include_numbers": "Incluir números",
|
||||
"include_symbols": "Incluir símbolos",
|
||||
"include_uppercase": "Incluir maiúsculas",
|
||||
"include_lowercase": "Incluir minúsculas",
|
||||
"generated_password": "Palavra-passe gerada",
|
||||
"refresh": "Atualizar",
|
||||
"cancel": "Cancelar",
|
||||
"insert": "Inserir"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML copiado!",
|
||||
"text": "Texto simples copiado!",
|
||||
"base64": "Base64 copiado!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -274,5 +274,55 @@
|
||||
"password_protected": "Zaščiteno z geslom",
|
||||
"ip_restricted": "Omejeno z IP",
|
||||
"average_max_views": "Povprečno največje število ogledov na skrivnost"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "Krepko",
|
||||
"italic": "Ležeče",
|
||||
"strikethrough": "Prečrtano",
|
||||
"inline_code": "Vrstična koda",
|
||||
"link": "Povezava",
|
||||
"remove_link": "Odstrani povezavo",
|
||||
"insert_password": "Vstavi geslo",
|
||||
"paragraph": "Odstavek",
|
||||
"heading1": "Naslov 1",
|
||||
"heading2": "Naslov 2",
|
||||
"heading3": "Naslov 3",
|
||||
"bullet_list": "Seznam z oznakami",
|
||||
"numbered_list": "Oštevilčen seznam",
|
||||
"blockquote": "Citat",
|
||||
"code_block": "Blok kode",
|
||||
"undo": "Razveljavi",
|
||||
"redo": "Ponovi",
|
||||
"copy_html": "Kopiraj kot HTML",
|
||||
"copy_text": "Kopiraj kot navadno besedilo",
|
||||
"copy_base64": "Kopiraj kot Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "Vstavi povezavo",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://primer.si",
|
||||
"cancel": "Prekliči",
|
||||
"insert": "Vstavi",
|
||||
"update": "Posodobi"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "Vstavi geslo",
|
||||
"length_label": "Dolžina gesla",
|
||||
"options_label": "Možnosti gesla",
|
||||
"include_numbers": "Vključi številke",
|
||||
"include_symbols": "Vključi simbole",
|
||||
"include_uppercase": "Vključi velike črke",
|
||||
"include_lowercase": "Vključi male črke",
|
||||
"generated_password": "Generirano geslo",
|
||||
"refresh": "Osveži",
|
||||
"cancel": "Prekliči",
|
||||
"insert": "Vstavi"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML kopiran!",
|
||||
"text": "Navadno besedilo kopirano!",
|
||||
"base64": "Base64 kopiran!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -273,5 +273,55 @@
|
||||
"password_protected": "密码保护",
|
||||
"ip_restricted": "IP限制",
|
||||
"average_max_views": "每个秘密的平均最大查看次数"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "粗体",
|
||||
"italic": "斜体",
|
||||
"strikethrough": "删除线",
|
||||
"inline_code": "内联代码",
|
||||
"link": "链接",
|
||||
"remove_link": "移除链接",
|
||||
"insert_password": "插入密码",
|
||||
"paragraph": "段落",
|
||||
"heading1": "标题1",
|
||||
"heading2": "标题2",
|
||||
"heading3": "标题3",
|
||||
"bullet_list": "无序列表",
|
||||
"numbered_list": "有序列表",
|
||||
"blockquote": "引用",
|
||||
"code_block": "代码块",
|
||||
"undo": "撤销",
|
||||
"redo": "重做",
|
||||
"copy_html": "复制为HTML",
|
||||
"copy_text": "复制为纯文本",
|
||||
"copy_base64": "复制为Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "插入链接",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://example.com",
|
||||
"cancel": "取消",
|
||||
"insert": "插入",
|
||||
"update": "更新"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "插入密码",
|
||||
"length_label": "密码长度",
|
||||
"options_label": "密码选项",
|
||||
"include_numbers": "包含数字",
|
||||
"include_symbols": "包含符号",
|
||||
"include_uppercase": "包含大写字母",
|
||||
"include_lowercase": "包含小写字母",
|
||||
"generated_password": "生成的密码",
|
||||
"refresh": "刷新",
|
||||
"cancel": "取消",
|
||||
"insert": "插入"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML已复制!",
|
||||
"text": "纯文本已复制!",
|
||||
"base64": "Base64已复制!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,5 +192,55 @@
|
||||
"password_protected": "密碼保護",
|
||||
"ip_restricted": "IP 限制",
|
||||
"average_max_views": "每個機密的平均最大瀏覽次數"
|
||||
},
|
||||
"editor": {
|
||||
"tooltips": {
|
||||
"bold": "粗體",
|
||||
"italic": "斜體",
|
||||
"strikethrough": "刪除線",
|
||||
"inline_code": "行內程式碼",
|
||||
"link": "連結",
|
||||
"remove_link": "移除連結",
|
||||
"insert_password": "插入密碼",
|
||||
"paragraph": "段落",
|
||||
"heading1": "標題1",
|
||||
"heading2": "標題2",
|
||||
"heading3": "標題3",
|
||||
"bullet_list": "項目符號列表",
|
||||
"numbered_list": "編號列表",
|
||||
"blockquote": "引用",
|
||||
"code_block": "程式碼區塊",
|
||||
"undo": "復原",
|
||||
"redo": "重做",
|
||||
"copy_html": "複製為HTML",
|
||||
"copy_text": "複製為純文字",
|
||||
"copy_base64": "複製為Base64"
|
||||
},
|
||||
"link_modal": {
|
||||
"title": "插入連結",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://example.com",
|
||||
"cancel": "取消",
|
||||
"insert": "插入",
|
||||
"update": "更新"
|
||||
},
|
||||
"password_modal": {
|
||||
"title": "插入密碼",
|
||||
"length_label": "密碼長度",
|
||||
"options_label": "密碼選項",
|
||||
"include_numbers": "包含數字",
|
||||
"include_symbols": "包含符號",
|
||||
"include_uppercase": "包含大寫字母",
|
||||
"include_lowercase": "包含小寫字母",
|
||||
"generated_password": "產生的密碼",
|
||||
"refresh": "重新整理",
|
||||
"cancel": "取消",
|
||||
"insert": "插入"
|
||||
},
|
||||
"copy_success": {
|
||||
"html": "HTML已複製!",
|
||||
"text": "純文字已複製!",
|
||||
"base64": "Base64已複製!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ const colors = require('tailwindcss/colors');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./client/**/*.{js,jsx}', , './client/**/*.html'],
|
||||
content: ['./client/**/*.{js,jsx}', './client/**/*.html'],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
@ -67,5 +67,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/forms')],
|
||||
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user