Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acf8ade23c | ||
|
|
aa884b49ce | ||
|
|
acb736e44f | ||
|
|
180886c588 | ||
|
|
e993476fad | ||
|
|
efaddeebb4 | ||
|
|
93f623a13a | ||
|
|
87a40cff61 | ||
|
|
f00c9b9a03 | ||
|
|
20b2ae436c | ||
|
|
820a4640da | ||
|
|
0193853035 | ||
|
|
b40344675e | ||
|
|
df7e687f61 | ||
|
|
ad10d26f13 | ||
|
|
588c64b380 | ||
|
|
3d3efc5e82 | ||
|
|
d8a20ebbd9 | ||
|
|
ebce8827ea |
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## [1.0.1](https://github.com/chartdb/chartdb/compare/v1.0.0...v1.0.1) (2024-11-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **offline:** add support when running on isolated network ([#359](https://github.com/chartdb/chartdb/issues/359)) ([aa884b4](https://github.com/chartdb/chartdb/commit/aa884b49ce16d70f67881bdc940993c1fe901796))
|
||||
* open default diagram after deleting current diagram ([#350](https://github.com/chartdb/chartdb/issues/350)) ([87a40cf](https://github.com/chartdb/chartdb/commit/87a40cff615b04b678642ba2d6e097c38b26d239))
|
||||
* **select-box:** allow using tab & space to show choices ([#336](https://github.com/chartdb/chartdb/issues/336)) ([93f623a](https://github.com/chartdb/chartdb/commit/93f623a13a61e9143638fbe7e8346f07e37a26b2))
|
||||
* **smart query:** import postgres FKs ([#357](https://github.com/chartdb/chartdb/issues/357)) ([acb736e](https://github.com/chartdb/chartdb/commit/acb736e44fd50d29a85b4eff42e20780aef710ed))
|
||||
* **templates:** add two more templates (Airbnb, Wordpress) ([#317](https://github.com/chartdb/chartdb/issues/317)) ([ebce882](https://github.com/chartdb/chartdb/commit/ebce8827eab049eefa0eebcb0ec2540698bc0e15))
|
||||
* **templates:** align database icon ([#351](https://github.com/chartdb/chartdb/issues/351)) ([efaddee](https://github.com/chartdb/chartdb/commit/efaddeebb4f24235d82f4e2bf7423fbf48b97187))
|
||||
* **template:** separator in case of empty url ([#355](https://github.com/chartdb/chartdb/issues/355)) ([180886c](https://github.com/chartdb/chartdb/commit/180886c5882f2329c797fc284b255012d21f5b5c))
|
||||
* **templates:** fetch templates data from router ([#321](https://github.com/chartdb/chartdb/issues/321)) ([d8a20eb](https://github.com/chartdb/chartdb/commit/d8a20ebbd9118989690a40fcd3aa59fb156b446f))
|
||||
|
||||
## 1.0.0 (2024-11-04)
|
||||
|
||||
|
||||
|
||||
10
README.md
@@ -15,8 +15,8 @@
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://discord.gg/QeFwyWSKwC">Community</a> •
|
||||
<a href="https://www.chartdb.io">Website</a> •
|
||||
<a href="https://app.chartdb.io/examples">Demo</a>
|
||||
<a href="https://www.chartdb.io?ref=github_readme">Website</a> •
|
||||
<a href="https://app.chartdb.io?ref=github_readme">Demo</a>
|
||||
</h3>
|
||||
|
||||
<h4 align="center">
|
||||
@@ -38,7 +38,7 @@
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img width='700px' src="./public/ChartDB.png">
|
||||
<img width='700px' src="./public/chartdb.png">
|
||||
</p>
|
||||
|
||||
### 🎉 ChartDB
|
||||
@@ -71,7 +71,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
|
||||
|
||||
## Getting Started
|
||||
|
||||
Use the [cloud version](https://app.chartdb.io/) or deploy locally:
|
||||
Use the [cloud version](https://app.chartdb.io?ref=github_readme_2) or deploy locally:
|
||||
|
||||
### How To Use
|
||||
|
||||
@@ -105,7 +105,7 @@ Open your browser and navigate to `http://localhost:8080`.
|
||||
|
||||
## Try it on our website
|
||||
|
||||
1. Go to [ChartDB.io](https://chartdb.io)
|
||||
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
|
||||
2. Click "Go to app"
|
||||
3. Choose the database that you are using.
|
||||
4. Take the magic query and run it in your database.
|
||||
|
||||
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "chartdb",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.51",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
|
Before Width: | Height: | Size: 882 KiB After Width: | Height: | Size: 882 KiB |
BIN
src/assets/templates/airbnb-dark.png
Normal file
|
After Width: | Height: | Size: 388 KiB |
BIN
src/assets/templates/airbnb.png
Normal file
|
After Width: | Height: | Size: 424 KiB |
BIN
src/assets/templates/pokemon-dark.png
Normal file
|
After Width: | Height: | Size: 421 KiB |
BIN
src/assets/templates/pokemon.png
Normal file
|
After Width: | Height: | Size: 444 KiB |
BIN
src/assets/templates/wordpress-db-dark.png
Normal file
|
After Width: | Height: | Size: 461 KiB |
BIN
src/assets/templates/wordpress-db.png
Normal file
|
After Width: | Height: | Size: 489 KiB |
@@ -9,6 +9,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DarkTheme } from './themes/dark';
|
||||
import { LightTheme } from './themes/light';
|
||||
import './config.ts';
|
||||
|
||||
export interface CodeSnippetProps {
|
||||
className?: string;
|
||||
|
||||
@@ -156,9 +156,19 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
[options, value, multiple]
|
||||
);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (!isOpen && e.code.toLowerCase() === 'space') {
|
||||
e.preventDefault();
|
||||
onOpenChange(true);
|
||||
}
|
||||
},
|
||||
[isOpen, onOpenChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
|
||||
<PopoverTrigger asChild>
|
||||
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
|
||||
<div
|
||||
className={cn(
|
||||
`flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''}`,
|
||||
|
||||
@@ -11,8 +11,6 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
import { useStorage } from '@/hooks/use-storage';
|
||||
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import type { DBSchema } from '@/lib/domain/db-schema';
|
||||
import {
|
||||
@@ -34,13 +32,11 @@ export const ChartDBProvider: React.FC<
|
||||
> = ({ children, diagram, readonly }) => {
|
||||
let db = useStorage();
|
||||
const events = useEventEmitter<ChartDBEvent>();
|
||||
const navigate = useNavigate();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
const { addUndoAction, resetRedoStack, resetUndoStack } =
|
||||
useRedoUndoStack();
|
||||
const [diagramId, setDiagramId] = useState('');
|
||||
const [diagramName, setDiagramName] = useState('');
|
||||
const { updateConfig } = useConfig();
|
||||
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
|
||||
const [diagramUpdatedAt, setDiagramUpdatedAt] = useState<Date>(new Date());
|
||||
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
||||
@@ -173,34 +169,13 @@ export const ChartDBProvider: React.FC<
|
||||
resetRedoStack();
|
||||
resetUndoStack();
|
||||
|
||||
const [config] = await Promise.all([
|
||||
db.getConfig(),
|
||||
await Promise.all([
|
||||
db.deleteDiagramTables(diagramId),
|
||||
db.deleteDiagramRelationships(diagramId),
|
||||
db.deleteDiagram(diagramId),
|
||||
db.deleteDiagramDependencies(diagramId),
|
||||
]);
|
||||
|
||||
if (config?.defaultDiagramId === diagramId) {
|
||||
const diagrams = await db.listDiagrams();
|
||||
|
||||
if (diagrams.length > 0) {
|
||||
const defaultDiagramId = diagrams[0].id;
|
||||
await updateConfig({ defaultDiagramId });
|
||||
navigate(`/diagrams/${defaultDiagramId}`);
|
||||
} else {
|
||||
await updateConfig({ defaultDiagramId: '' });
|
||||
navigate('/');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
db,
|
||||
diagramId,
|
||||
navigate,
|
||||
resetRedoStack,
|
||||
resetUndoStack,
|
||||
updateConfig,
|
||||
]);
|
||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||
|
||||
const updateDiagramUpdatedAt: ChartDBContext['updateDiagramUpdatedAt'] =
|
||||
useCallback(async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const HelmetData: React.FC = () => (
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://app.chartdb.io/ChartDB.png"
|
||||
content="https://app.chartdb.io/chartdb.png"
|
||||
/>
|
||||
<meta property="og:url" content="https://app.chartdb.io" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
@@ -32,7 +32,7 @@ export const HelmetData: React.FC = () => (
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://github.com/chartdb/chartdb/raw/main/public/ChartDB.png"
|
||||
content="https://github.com/chartdb/chartdb/raw/main/public/chartdb.png"
|
||||
/>
|
||||
<title>ChartDB - Database schema diagrams visualizer</title>
|
||||
</Helmet>
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import type { LanguageMetadata } from './types';
|
||||
import { en, enMetadata } from './locales/en';
|
||||
import { es } from './locales/es';
|
||||
import { fr } from './locales/fr';
|
||||
import { de } from './locales/de';
|
||||
import { hi } from './locales/hi';
|
||||
import { ja } from './locales/ja';
|
||||
import { pt_BR } from './locales/pt_BR';
|
||||
import { es, esMetadata } from './locales/es';
|
||||
import { fr, frMetadata } from './locales/fr';
|
||||
import { de, deMetadata } from './locales/de';
|
||||
import { hi, hiMetadata } from './locales/hi';
|
||||
import { ja, jaMetadata } from './locales/ja';
|
||||
import { pt_BR, pt_BRMetadata } from './locales/pt_BR';
|
||||
import { uk, ukMetadata } from './locales/uk';
|
||||
|
||||
export const languages: LanguageMetadata[] = [
|
||||
enMetadata,
|
||||
esMetadata,
|
||||
frMetadata,
|
||||
deMetadata,
|
||||
hiMetadata,
|
||||
jaMetadata,
|
||||
pt_BRMetadata,
|
||||
ukMetadata,
|
||||
];
|
||||
|
||||
const resources = {
|
||||
en,
|
||||
@@ -16,6 +29,7 @@ const resources = {
|
||||
hi,
|
||||
ja,
|
||||
pt_BR,
|
||||
uk,
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
|
||||
354
src/i18n/locales/uk.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
import type { LanguageMetadata, LanguageTranslation } from '../types';
|
||||
|
||||
export const uk: LanguageTranslation = {
|
||||
translation: {
|
||||
menu: {
|
||||
file: {
|
||||
file: 'файл',
|
||||
new: 'новий',
|
||||
open: 'відкрити',
|
||||
save: 'зберегти',
|
||||
import_database: 'Імпорт бази даних',
|
||||
export_sql: 'Експорт SQL',
|
||||
export_as: 'Експортувати як',
|
||||
delete_diagram: 'Видалити діаграму',
|
||||
exit: 'вийти',
|
||||
},
|
||||
edit: {
|
||||
edit: 'редагувати',
|
||||
undo: 'Скасувати',
|
||||
redo: 'Повторити',
|
||||
clear: 'очистити',
|
||||
},
|
||||
view: {
|
||||
view: 'переглянути',
|
||||
show_sidebar: 'Показати бічну панель',
|
||||
hide_sidebar: 'Приховати бічну панель',
|
||||
hide_cardinality: 'Приховати потужність',
|
||||
show_cardinality: 'Показати кардинальність',
|
||||
zoom_on_scroll: 'Збільшити прокручування',
|
||||
theme: 'Тема',
|
||||
change_language: 'Мова',
|
||||
show_dependencies: 'Показати залежності',
|
||||
hide_dependencies: 'Приховати залежності',
|
||||
},
|
||||
help: {
|
||||
help: 'Допомога',
|
||||
visit_website: 'Відвідайте ChartDB',
|
||||
join_discord: 'Приєднуйтесь до нас в Діскорд',
|
||||
schedule_a_call: 'Поговоріть з нами!',
|
||||
},
|
||||
},
|
||||
|
||||
delete_diagram_alert: {
|
||||
title: 'Видалити діаграму',
|
||||
description:
|
||||
'Цю дію не можна скасувати. Це призведе до остаточного видалення діаграми.',
|
||||
cancel: 'Скасувати',
|
||||
delete: 'Видалити',
|
||||
},
|
||||
|
||||
clear_diagram_alert: {
|
||||
title: 'Чітка діаграма',
|
||||
description:
|
||||
'Цю дію не можна скасувати. Це назавжди видалить усі дані на діаграмі.',
|
||||
cancel: 'Скасувати',
|
||||
clear: 'очистити',
|
||||
},
|
||||
|
||||
reorder_diagram_alert: {
|
||||
title: 'Діаграма зміни порядку',
|
||||
description:
|
||||
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
|
||||
reorder: 'Змінити порядок',
|
||||
cancel: 'Скасувати',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Кілька схем',
|
||||
description:
|
||||
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
|
||||
dont_show_again: 'Більше не показувати',
|
||||
change_schema: 'Зміна',
|
||||
none: 'немає',
|
||||
},
|
||||
|
||||
theme: {
|
||||
system: 'система',
|
||||
light: 'світлий',
|
||||
dark: 'Темний',
|
||||
},
|
||||
|
||||
zoom: {
|
||||
on: 'увімкнути',
|
||||
off: 'вимкнути',
|
||||
},
|
||||
|
||||
last_saved: 'Востаннє збережено',
|
||||
saved: 'Збережено',
|
||||
diagrams: 'Діаграми',
|
||||
loading_diagram: 'Діаграма завантаження...',
|
||||
deselect_all: 'Зняти вибір із усіх',
|
||||
select_all: 'Вибрати усі',
|
||||
clear: 'Очистити',
|
||||
show_more: 'показати більше',
|
||||
show_less: 'Показати менше',
|
||||
copy_to_clipboard: 'Копіювати в буфер обміну',
|
||||
copied: 'Скопійовано!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Схема:',
|
||||
filter_by_schema: 'Фільтрувати за схемою',
|
||||
search_schema: 'Схема пошуку...',
|
||||
no_schemas_found: 'Схеми не знайдено.',
|
||||
view_all_options: 'Переглянути всі параметри...',
|
||||
tables_section: {
|
||||
tables: 'Таблиці',
|
||||
add_table: 'Додати таблицю',
|
||||
filter: 'фільтр',
|
||||
collapse: 'Згорнути все',
|
||||
|
||||
table: {
|
||||
fields: 'поля',
|
||||
nullable: 'Зведений нанівець?',
|
||||
primary_key: 'Первинний ключ',
|
||||
indexes: 'Індекси',
|
||||
comments: 'Коментарі',
|
||||
no_comments: 'Без коментарів',
|
||||
add_field: 'Додати поле',
|
||||
add_index: 'Додати індекс',
|
||||
index_select_fields: 'Виберіть поля',
|
||||
no_types_found: 'Типи не знайдено',
|
||||
field_name: "Ім'я",
|
||||
field_type: 'Тип',
|
||||
field_actions: {
|
||||
title: 'Атрибути полів',
|
||||
unique: 'Унікальний',
|
||||
comments: 'Коментарі',
|
||||
no_comments: 'Без коментарів',
|
||||
delete_field: 'Видалити поле',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Атрибути індексу',
|
||||
name: "Ім'я",
|
||||
unique: 'Унікальний',
|
||||
delete_index: 'Видалити індекс',
|
||||
},
|
||||
table_actions: {
|
||||
title: 'Дії таблиці',
|
||||
change_schema: 'Змінити схему',
|
||||
add_field: 'Додати поле',
|
||||
add_index: 'Додати індекс',
|
||||
delete_table: 'Видалити таблицю',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'Без таблиць',
|
||||
description: 'Щоб почати, створіть таблицю',
|
||||
},
|
||||
},
|
||||
relationships_section: {
|
||||
relationships: 'стосунки',
|
||||
filter: 'фільтр',
|
||||
add_relationship: "Додати зв'язок",
|
||||
collapse: 'Згорнути все',
|
||||
relationship: {
|
||||
primary: 'Первинна таблиця',
|
||||
foreign: 'Посилання на таблицю',
|
||||
cardinality: 'Кардинальність',
|
||||
delete_relationship: 'Видалити',
|
||||
relationship_actions: {
|
||||
title: 'Дії',
|
||||
delete_relationship: 'Видалити',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'Жодних стосунків',
|
||||
description: 'Створіть зв’язок для з’єднання таблиць',
|
||||
},
|
||||
},
|
||||
dependencies_section: {
|
||||
dependencies: 'Залежності',
|
||||
filter: 'фільтр',
|
||||
collapse: 'Згорнути все',
|
||||
dependency: {
|
||||
table: 'Таблиця',
|
||||
dependent_table: 'Залежний вид',
|
||||
delete_dependency: 'Видалити',
|
||||
dependency_actions: {
|
||||
title: 'Дії',
|
||||
delete_dependency: 'Видалити',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'Жодних залежностей',
|
||||
description: 'Створіть подання, щоб почати',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
zoom_in: 'Збільшити',
|
||||
zoom_out: 'Зменшити',
|
||||
save: 'зберегти',
|
||||
show_all: 'Показати все',
|
||||
undo: 'Скасувати',
|
||||
redo: 'Повторити',
|
||||
reorder_diagram: 'Діаграма зміни порядку',
|
||||
highlight_overlapping_tables: 'Виділіть таблиці, що перекриваються',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
database_selection: {
|
||||
title: 'Що таке ваша база даних?',
|
||||
description:
|
||||
'Кожна база даних має свої унікальні особливості та можливості.',
|
||||
check_examples_long: 'Перевірте приклади',
|
||||
check_examples_short: 'Приклади',
|
||||
},
|
||||
|
||||
import_database: {
|
||||
title: 'Імпортуйте вашу базу даних',
|
||||
database_edition: 'Редакція бази даних:',
|
||||
step_1: 'Запустіть цей сценарій у своїй базі даних:',
|
||||
step_2: 'Вставте сюди результат сценарію:',
|
||||
script_results_placeholder: 'Результати сценарію тут...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS Інструкції',
|
||||
title: 'Інструкції',
|
||||
step_1: 'Перейдіть до Інструменти > Опції > Результати запиту > SQL Сервер.',
|
||||
step_2: 'Якщо ви використовуєте «Результати в сітку», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
|
||||
},
|
||||
instructions_link: 'Потрібна допомога? Подивіться як',
|
||||
check_script_result: 'Перевірте результат сценарію',
|
||||
},
|
||||
|
||||
cancel: 'Скасувати',
|
||||
back: 'Назад',
|
||||
empty_diagram: 'Порожня діаграма',
|
||||
continue: 'Продовжити',
|
||||
import: 'Імпорт',
|
||||
},
|
||||
|
||||
open_diagram_dialog: {
|
||||
title: 'Відкрита діаграма',
|
||||
description:
|
||||
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
|
||||
table_columns: {
|
||||
name: "Ім'я",
|
||||
created_at: 'Створено в',
|
||||
last_modified: 'Востаннє змінено',
|
||||
tables_count: 'Таблиці',
|
||||
},
|
||||
cancel: 'Скасувати',
|
||||
open: 'Відкрити',
|
||||
},
|
||||
|
||||
export_sql_dialog: {
|
||||
title: 'Експорт SQL',
|
||||
description:
|
||||
'Експортуйте свою схему діаграми в {{databaseType}} сценарій',
|
||||
close: 'Закрити',
|
||||
loading: {
|
||||
text: 'ШІ створює SQL для {{databaseType}}...',
|
||||
description: 'Це має зайняти до 30 секунд.',
|
||||
},
|
||||
error: {
|
||||
message:
|
||||
"Помилка створення сценарію SQL. Спробуйте пізніше або <0>зв'яжіться з нами</0>.",
|
||||
description:
|
||||
'Не соромтеся використовувати свій OPENAI_TOKEN, дивіться посібник <0>тут</0>.',
|
||||
},
|
||||
},
|
||||
|
||||
create_relationship_dialog: {
|
||||
title: 'Створити відносини',
|
||||
primary_table: 'Первинна таблиця',
|
||||
primary_field: 'Первинне поле',
|
||||
referenced_table: 'Посилання на таблицю',
|
||||
referenced_field: 'Поле посилання',
|
||||
primary_table_placeholder: 'Виберіть таблицю',
|
||||
primary_field_placeholder: 'Виберіть поле',
|
||||
referenced_table_placeholder: 'Виберіть таблицю',
|
||||
referenced_field_placeholder: 'Виберіть поле',
|
||||
no_tables_found: 'Таблиці не знайдено',
|
||||
no_fields_found: 'Поля не знайдено',
|
||||
create: 'Створити',
|
||||
cancel: 'Скасувати',
|
||||
},
|
||||
|
||||
import_database_dialog: {
|
||||
title: 'Імпорт до поточної діаграми',
|
||||
override_alert: {
|
||||
title: 'Імпорт бази даних',
|
||||
content: {
|
||||
alert: 'Імпортування цієї діаграми вплине на наявні таблиці та зв’язки.',
|
||||
new_tables:
|
||||
'<bold>{{newTablesNumber}}</bold> будуть додані нові таблиці.',
|
||||
new_relationships:
|
||||
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові відносини.',
|
||||
tables_override:
|
||||
'<bold>{{tablesOverrideNumber}}</bold> таблиці будуть перезаписані.',
|
||||
proceed: 'Ви хочете продовжити?',
|
||||
},
|
||||
import: 'Імпорт',
|
||||
cancel: 'Скасувати',
|
||||
},
|
||||
},
|
||||
|
||||
export_image_dialog: {
|
||||
title: 'Експорт зображення',
|
||||
description: 'Виберіть коефіцієнт масштабування для експорту:',
|
||||
scale_1x: '1x Регулярний',
|
||||
scale_2x: '2x (Рекомендовано)',
|
||||
scale_3x: '3x',
|
||||
scale_4x: '4x',
|
||||
cancel: 'Скасувати',
|
||||
export: 'Експорт',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
title: 'Виберіть Схему',
|
||||
description:
|
||||
'Наразі відображається кілька схем. Виберіть один для нової таблиці.',
|
||||
cancel: 'Скасувати',
|
||||
confirm: 'Підтвердити',
|
||||
},
|
||||
|
||||
update_table_schema_dialog: {
|
||||
title: 'Змінити схему',
|
||||
description: 'Оновити таблицю "{{tableName}}" схему',
|
||||
cancel: 'Скасувати',
|
||||
confirm: 'Змінити',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'Допоможіть нам покращитися!',
|
||||
description: 'Хочете позначити нас на Ґітхаб? Це лише один клік!',
|
||||
close: 'Не зараз',
|
||||
confirm: 'звичайно!',
|
||||
},
|
||||
|
||||
relationship_type: {
|
||||
one_to_one: 'Один до одного',
|
||||
one_to_many: 'Один до багатьох',
|
||||
many_to_one: 'Багато до одного',
|
||||
many_to_many: 'Багато до багатьох',
|
||||
},
|
||||
|
||||
canvas_context_menu: {
|
||||
new_table: 'Нова таблиця',
|
||||
new_relationship: 'Нові стосунки',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
edit_table: 'Редагувати таблицю',
|
||||
delete_table: 'Видалити таблицю',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ukMetadata: LanguageMetadata = {
|
||||
name: 'Українська',
|
||||
code: 'uk',
|
||||
};
|
||||
@@ -68,29 +68,46 @@ WITH fk_info${databaseEdition ? '_' + databaseEdition : ''} AS (
|
||||
',"fk_def":"', replace(fk_def, '"', ''),
|
||||
'"}')), ',') as fk_metadata
|
||||
FROM (
|
||||
SELECT connamespace::regnamespace::text AS schema_name,
|
||||
conname AS foreign_key_name,
|
||||
CASE
|
||||
WHEN strpos(conrelid::regclass::text, '.') > 0
|
||||
THEN split_part(conrelid::regclass::text, '.', 2)
|
||||
ELSE conrelid::regclass::text
|
||||
END AS table_name,
|
||||
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[1] AS fk_column,
|
||||
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[2] AS reference_schema,
|
||||
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[3] AS reference_table,
|
||||
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[4] AS reference_column,
|
||||
pg_get_constraintdef(oid) as fk_def
|
||||
FROM
|
||||
pg_constraint
|
||||
WHERE
|
||||
contype = 'f'
|
||||
AND connamespace::regnamespace::text NOT IN ('information_schema', 'pg_catalog')${
|
||||
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
|
||||
? timescaleFilters
|
||||
: databaseEdition === DatabaseEdition.POSTGRESQL_SUPABASE
|
||||
? supabaseFilters
|
||||
: ''
|
||||
}
|
||||
SELECT c.conname AS foreign_key_name,
|
||||
n.nspname AS schema_name,
|
||||
CASE
|
||||
WHEN position('.' in conrelid::regclass::text) > 0
|
||||
THEN split_part(conrelid::regclass::text, '.', 2)
|
||||
ELSE conrelid::regclass::text
|
||||
END AS table_name,
|
||||
a.attname AS fk_column,
|
||||
nr.nspname AS reference_schema,
|
||||
CASE
|
||||
WHEN position('.' in confrelid::regclass::text) > 0
|
||||
THEN split_part(confrelid::regclass::text, '.', 2)
|
||||
ELSE confrelid::regclass::text
|
||||
END AS reference_table,
|
||||
af.attname AS reference_column,
|
||||
pg_get_constraintdef(c.oid) as fk_def
|
||||
FROM
|
||||
pg_constraint AS c
|
||||
JOIN
|
||||
pg_attribute AS a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
|
||||
JOIN
|
||||
pg_class AS cl ON cl.oid = c.conrelid
|
||||
JOIN
|
||||
pg_namespace AS n ON n.oid = cl.relnamespace
|
||||
JOIN
|
||||
pg_attribute AS af ON af.attnum = ANY(c.confkey) AND af.attrelid = c.confrelid
|
||||
JOIN
|
||||
pg_class AS clf ON clf.oid = c.confrelid
|
||||
JOIN
|
||||
pg_namespace AS nr ON nr.oid = clf.relnamespace
|
||||
WHERE
|
||||
c.contype = 'f'
|
||||
AND connamespace::regnamespace::text NOT IN ('information_schema', 'pg_catalog')${
|
||||
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
|
||||
? timescaleFilters
|
||||
: databaseEdition ===
|
||||
DatabaseEdition.POSTGRESQL_SUPABASE
|
||||
? supabaseFilters
|
||||
: ''
|
||||
}
|
||||
) AS x
|
||||
), pk_info AS (
|
||||
SELECT array_to_string(array_agg(CONCAT('{"schema":"', replace(schema_name, '"', ''), '"',
|
||||
|
||||
@@ -35,6 +35,7 @@ import { DialogProvider } from '@/context/dialog-context/dialog-provider';
|
||||
import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts-provider';
|
||||
import { Spinner } from '@/components/spinner/spinner';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useStorage } from '@/hooks/use-storage';
|
||||
|
||||
const OPEN_STAR_US_AFTER_SECONDS = 30;
|
||||
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
|
||||
@@ -73,6 +74,7 @@ const EditorPageComponent: React.FC = () => {
|
||||
} = useLocalConfig();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { listDiagrams } = useStorage();
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) {
|
||||
@@ -106,7 +108,15 @@ const EditorPageComponent: React.FC = () => {
|
||||
navigate(`/diagrams/${config.defaultDiagramId}`);
|
||||
}
|
||||
} else {
|
||||
openCreateDiagramDialog();
|
||||
const diagrams = await listDiagrams();
|
||||
|
||||
if (diagrams.length > 0) {
|
||||
const defaultDiagramId = diagrams[0].id;
|
||||
await updateConfig({ defaultDiagramId });
|
||||
navigate(`/diagrams/${defaultDiagramId}`);
|
||||
} else {
|
||||
openCreateDiagramDialog();
|
||||
}
|
||||
}
|
||||
};
|
||||
loadDefaultDiagram();
|
||||
@@ -115,6 +125,7 @@ const EditorPageComponent: React.FC = () => {
|
||||
openCreateDiagramDialog,
|
||||
config,
|
||||
navigate,
|
||||
listDiagrams,
|
||||
loadDiagram,
|
||||
resetRedoStack,
|
||||
resetUndoStack,
|
||||
|
||||
@@ -30,16 +30,11 @@ import { useHistory } from '@/hooks/use-history';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { enMetadata } from '@/i18n/locales/en';
|
||||
import { esMetadata } from '@/i18n/locales/es';
|
||||
import { deMetadata } from '@/i18n/locales/de';
|
||||
import { jaMetadata } from '@/i18n/locales/ja';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { frMetadata } from '@/i18n/locales/fr';
|
||||
import { hiMetadata } from '@/i18n/locales/hi';
|
||||
import { DiagramName } from './diagram-name';
|
||||
import { LastSaved } from './last-saved';
|
||||
import { pt_BRMetadata } from '@/i18n/locales/pt_BR';
|
||||
import { languages } from '@/i18n/i18n';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export interface TopNavbarProps {}
|
||||
|
||||
@@ -70,6 +65,12 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
||||
const { isMd: isDesktop } = useBreakpoint('md');
|
||||
const { config, updateConfig } = useConfig();
|
||||
const { exportImage } = useExportImage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleDeleteDiagramAction = useCallback(() => {
|
||||
deleteDiagram();
|
||||
navigate('/');
|
||||
}, [deleteDiagram, navigate]);
|
||||
|
||||
const createNewDiagram = () => {
|
||||
openCreateDiagramDialog();
|
||||
@@ -429,7 +430,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
||||
closeLabel: t(
|
||||
'delete_diagram_alert.cancel'
|
||||
),
|
||||
onAction: deleteDiagram,
|
||||
onAction: handleDeleteDiagramAction,
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -565,85 +566,22 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
||||
{t('menu.view.change_language')}
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarCheckboxItem
|
||||
onClick={() =>
|
||||
changeLanguage(enMetadata.code)
|
||||
}
|
||||
checked={
|
||||
i18n.language ===
|
||||
enMetadata.code
|
||||
}
|
||||
>
|
||||
{enMetadata.name}
|
||||
</MenubarCheckboxItem>
|
||||
<MenubarCheckboxItem
|
||||
onClick={() =>
|
||||
changeLanguage(esMetadata.code)
|
||||
}
|
||||
checked={
|
||||
i18n.language ===
|
||||
esMetadata.code
|
||||
}
|
||||
>
|
||||
{esMetadata.name}
|
||||
</MenubarCheckboxItem>
|
||||
<MenubarCheckboxItem
|
||||
onClick={() =>
|
||||
changeLanguage(frMetadata.code)
|
||||
}
|
||||
checked={
|
||||
i18n.language ===
|
||||
frMetadata.code
|
||||
}
|
||||
>
|
||||
{frMetadata.name}
|
||||
</MenubarCheckboxItem>
|
||||
<MenubarCheckboxItem
|
||||
onClick={() =>
|
||||
changeLanguage(deMetadata.code)
|
||||
}
|
||||
checked={
|
||||
i18n.language ===
|
||||
deMetadata.code
|
||||
}
|
||||
>
|
||||
{deMetadata.name}
|
||||
</MenubarCheckboxItem>
|
||||
<MenubarCheckboxItem
|
||||
onClick={() =>
|
||||
changeLanguage(hiMetadata.code)
|
||||
}
|
||||
checked={
|
||||
i18n.language ===
|
||||
hiMetadata.code
|
||||
}
|
||||
>
|
||||
{hiMetadata.name}
|
||||
</MenubarCheckboxItem>
|
||||
<MenubarCheckboxItem
|
||||
onClick={() =>
|
||||
changeLanguage(jaMetadata.code)
|
||||
}
|
||||
checked={
|
||||
i18n.language ===
|
||||
jaMetadata.code
|
||||
}
|
||||
>
|
||||
{jaMetadata.name}
|
||||
</MenubarCheckboxItem>
|
||||
<MenubarCheckboxItem
|
||||
onClick={() =>
|
||||
changeLanguage(
|
||||
pt_BRMetadata.code
|
||||
)
|
||||
}
|
||||
checked={
|
||||
i18n.language ===
|
||||
pt_BRMetadata.code
|
||||
}
|
||||
>
|
||||
{pt_BRMetadata.name}
|
||||
</MenubarCheckboxItem>
|
||||
{languages.map((language) => (
|
||||
<MenubarCheckboxItem
|
||||
key={language.code}
|
||||
onClick={() =>
|
||||
changeLanguage(
|
||||
language.code
|
||||
)
|
||||
}
|
||||
checked={
|
||||
i18n.language ===
|
||||
language.code
|
||||
}
|
||||
>
|
||||
{language.name}
|
||||
</MenubarCheckboxItem>
|
||||
))}
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
</MenubarContent>
|
||||
|
||||
@@ -32,6 +32,7 @@ import { ReactFlowProvider } from '@xyflow/react';
|
||||
import { ChartDBProvider } from '@/context/chartdb-context/chartdb-provider';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { APP_URL, HOST_URL } from '@/lib/env';
|
||||
import { Link } from '@/components/link/link';
|
||||
|
||||
export interface TemplatePageLoaderData {
|
||||
template: Template | undefined;
|
||||
@@ -65,25 +66,22 @@ const TemplatePageComponent: React.FC = () => {
|
||||
<Helmet>
|
||||
{template ? (
|
||||
<>
|
||||
{HOST_URL !== 'https://chartdb.io' ? (
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`https://chartdb.io/templates/${templateSlug}`}
|
||||
/>
|
||||
) : null}
|
||||
<title>
|
||||
Database schema diagram for {template.name} |
|
||||
ChartDB
|
||||
{`Database schema diagram for - ${template.name} | ChartDB`}
|
||||
</title>
|
||||
<meta
|
||||
name="title"
|
||||
content={`Database schema for - ${template.name} | ChartDB`}
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content={`${template.shortDescription}: ${template.description}`}
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content={`${template.keywords.join(', ')}`}
|
||||
/>
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`Database schema for - ${template.name} | ChartDB`}
|
||||
content={`Database schema diagram for - ${template.name} | ChartDB`}
|
||||
/>
|
||||
<meta
|
||||
property="og:url"
|
||||
@@ -98,7 +96,7 @@ const TemplatePageComponent: React.FC = () => {
|
||||
content={`${HOST_URL}${template.image}`}
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta property="og:site_name" content="ChartDB" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content={`Database schema for - ${template.name} | ChartDB`}
|
||||
@@ -115,6 +113,8 @@ const TemplatePageComponent: React.FC = () => {
|
||||
name="twitter:card"
|
||||
content="summary_large_image"
|
||||
/>
|
||||
<meta name="twitter:site" content="@ChartDB_io" />
|
||||
<meta name="twitter:creator" content="@ChartDB_io" />
|
||||
</>
|
||||
) : (
|
||||
<title>Database Schema Diagram | ChartDB</title>
|
||||
@@ -174,8 +174,11 @@ const TemplatePageComponent: React.FC = () => {
|
||||
</Breadcrumb>
|
||||
<div className="flex flex-col items-center gap-4 md:flex-row md:items-start md:justify-between md:gap-0">
|
||||
<div className="flex flex-col pr-0 md:pr-20">
|
||||
<h1 className="font-primary text-2xl font-bold">
|
||||
<h1 className="flex flex-col font-primary text-2xl font-bold">
|
||||
{template?.name}
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
Database schema diagram
|
||||
</span>
|
||||
</h1>
|
||||
<h2 className="mt-3">
|
||||
<span className="font-semibold">
|
||||
@@ -244,6 +247,25 @@ const TemplatePageComponent: React.FC = () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
{template.url ? (
|
||||
<>
|
||||
<div>
|
||||
<h4 className="mb-1 text-base font-semibold md:text-left">
|
||||
Url
|
||||
</h4>
|
||||
|
||||
<Link
|
||||
className="break-all text-sm text-muted-foreground"
|
||||
href={`${template.url}?ref=chartdb`}
|
||||
target="_blank"
|
||||
>
|
||||
{template.url}
|
||||
</Link>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
) : null}
|
||||
<div>
|
||||
<h4 className="mb-1 text-base font-semibold md:text-left">
|
||||
Tags
|
||||
|
||||
@@ -26,7 +26,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({ template }) => {
|
||||
className="h-2 rounded-t-[6px]"
|
||||
style={{ backgroundColor: randomColor() }}
|
||||
></div>
|
||||
<div className="grow overflow-hidden p-1">
|
||||
<div className="overflow-hidden p-1">
|
||||
<img
|
||||
src={
|
||||
effectiveTheme === 'dark'
|
||||
@@ -43,7 +43,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({ template }) => {
|
||||
{template.name}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex h-full flex-col justify-start pt-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="mr-1">
|
||||
<img
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import ChartDBLogo from '@/assets/logo-light.png';
|
||||
import ChartDBDarkLogo from '@/assets/logo-dark.png';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
@@ -7,54 +7,67 @@ import { ThemeProvider } from '@/context/theme-context/theme-provider';
|
||||
import { Component, Star } from 'lucide-react';
|
||||
import { ListMenu } from '@/components/list-menu/list-menu';
|
||||
import { TemplateCard } from './template-card/template-card';
|
||||
import { useMatches, useParams } from 'react-router-dom';
|
||||
import { useLoaderData, useMatches, useParams } from 'react-router-dom';
|
||||
import type { Template } from '@/templates-data/templates-data';
|
||||
import { Spinner } from '@/components/spinner/spinner';
|
||||
import { removeDups } from '@/lib/utils';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { HOST_URL } from '@/lib/env';
|
||||
|
||||
export interface TemplatesPageLoaderData {
|
||||
templates: Template[] | undefined;
|
||||
allTags: string[] | undefined;
|
||||
}
|
||||
|
||||
const TemplatesPageComponent: React.FC = () => {
|
||||
const { effectiveTheme } = useTheme();
|
||||
const data = useLoaderData() as TemplatesPageLoaderData;
|
||||
|
||||
const { templates, allTags } = data ?? {};
|
||||
const { tag } = useParams<{ tag: string }>();
|
||||
const matches = useMatches();
|
||||
const [templates, setTemplates] = React.useState<Template[]>();
|
||||
const [tags, setTags] = React.useState<string[]>();
|
||||
|
||||
const isFeatured = matches.some(
|
||||
(match) => match.id === 'templates_featured'
|
||||
);
|
||||
const isAllTemplates = matches.some((match) => match.id === 'templates');
|
||||
const isTags = matches.some((match) => match.id === 'templates_tags');
|
||||
|
||||
useEffect(() => {
|
||||
const loadTemplates = async () => {
|
||||
const { templates: loadedTemplates } = await import(
|
||||
'@/templates-data/templates-data'
|
||||
);
|
||||
|
||||
let templatesToLoad = loadedTemplates;
|
||||
|
||||
if (isFeatured) {
|
||||
templatesToLoad = loadedTemplates.filter((t) => t.featured);
|
||||
}
|
||||
|
||||
if (isTags && tag) {
|
||||
templatesToLoad = loadedTemplates.filter((t) =>
|
||||
t.tags.includes(tag)
|
||||
);
|
||||
}
|
||||
|
||||
setTemplates(templatesToLoad);
|
||||
setTags(removeDups(loadedTemplates?.flatMap((t) => t.tags) ?? []));
|
||||
};
|
||||
|
||||
loadTemplates();
|
||||
}, [isFeatured, isTags, tag]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>ChartDB - Database Schema Templates</title>
|
||||
{HOST_URL !== 'https://chartdb.io' ? (
|
||||
<link rel="canonical" href="https://chartdb.io/templates" />
|
||||
) : null}
|
||||
<title>Database Schema Diagram Templates | ChartDB</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Discover a collection of real-world database schema diagrams, featuring example applications and popular open-source projects."
|
||||
/>
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Database Schema Diagram Templates | ChartDB"
|
||||
/>
|
||||
<meta property="og:url" content={`${HOST_URL}/templates`} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Discover a collection of real-world database schema diagrams, featuring example applications and popular open-source projects."
|
||||
/>
|
||||
<meta property="og:image" content={`${HOST_URL}/chartdb.png`} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="ChartDB" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="Database Schema Diagram Templates | ChartDB"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Discover a collection of real-world database schema diagrams, featuring example applications and popular open-source projects."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content={`${HOST_URL}/chartdb.png`}
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@ChartDB_io" />
|
||||
<meta name="twitter:creator" content="@ChartDB_io" />
|
||||
</Helmet>
|
||||
|
||||
<section className="flex w-screen flex-col bg-background">
|
||||
@@ -92,10 +105,9 @@ const TemplatesPageComponent: React.FC = () => {
|
||||
Database Schema Templates
|
||||
</h1>
|
||||
<h2 className="mt-1 font-primary text-base text-muted-foreground">
|
||||
Explore a collection of real-world database schemas
|
||||
drawn from real-world live applications and open-source
|
||||
projects. Use these as a foundation or source of
|
||||
inspiration when designing your app’s architecture.
|
||||
Discover a collection of real-world database schema
|
||||
diagrams, featuring example applications and popular
|
||||
open-source projects.
|
||||
</h2>
|
||||
{!templates ? (
|
||||
<Spinner
|
||||
@@ -125,10 +137,10 @@ const TemplatesPageComponent: React.FC = () => {
|
||||
<h4 className="mt-4 text-left text-sm font-semibold">
|
||||
Tags
|
||||
</h4>
|
||||
{tags ? (
|
||||
{allTags ? (
|
||||
<ListMenu
|
||||
className="mt-1 w-44 shrink-0"
|
||||
items={tags.map((currentTag) => ({
|
||||
items={allTags.map((currentTag) => ({
|
||||
title: currentTag,
|
||||
href: `/templates/tags/${currentTag}`,
|
||||
selected: tag === currentTag,
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import type { TemplatePageLoaderData } from './pages/template-page/template-page';
|
||||
import type { TemplatesPageLoaderData } from './pages/templates-page/templates-page';
|
||||
import { getTemplatesAndAllTags } from './templates-data/template-utils';
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
...['', 'diagrams/:diagramId'].map((path) => ({
|
||||
@@ -38,6 +40,15 @@ const routes: RouteObject[] = [
|
||||
element: <TemplatesPage />,
|
||||
};
|
||||
},
|
||||
|
||||
loader: async (): Promise<TemplatesPageLoaderData> => {
|
||||
const { tags, templates } = await getTemplatesAndAllTags();
|
||||
|
||||
return {
|
||||
allTags: tags,
|
||||
templates,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'templates_featured',
|
||||
@@ -50,6 +61,16 @@ const routes: RouteObject[] = [
|
||||
element: <TemplatesPage />,
|
||||
};
|
||||
},
|
||||
loader: async (): Promise<TemplatesPageLoaderData> => {
|
||||
const { tags, templates } = await getTemplatesAndAllTags({
|
||||
featured: true,
|
||||
});
|
||||
|
||||
return {
|
||||
allTags: tags,
|
||||
templates,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'templates_tags',
|
||||
@@ -62,6 +83,16 @@ const routes: RouteObject[] = [
|
||||
element: <TemplatesPage />,
|
||||
};
|
||||
},
|
||||
loader: async ({ params }): Promise<TemplatesPageLoaderData> => {
|
||||
const { tags, templates } = await getTemplatesAndAllTags({
|
||||
tag: params.tag,
|
||||
});
|
||||
|
||||
return {
|
||||
allTags: tags,
|
||||
templates,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'templates_templateSlug',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { Template } from './templates-data';
|
||||
import { generateId } from '@/lib/utils';
|
||||
import { generateId, removeDups } from '@/lib/utils';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DBIndex } from '@/lib/domain/db-index';
|
||||
@@ -87,3 +87,30 @@ export const convertTemplateToNewDiagram = (template: Template): Diagram => {
|
||||
tables,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTemplatesAndAllTags = async ({
|
||||
featured,
|
||||
tag,
|
||||
}: {
|
||||
featured?: boolean;
|
||||
tag?: string;
|
||||
} = {}): Promise<{ templates: Template[]; tags: string[] }> => {
|
||||
const { templates } = await import('@/templates-data/templates-data');
|
||||
const allTags = removeDups(templates?.flatMap((t) => t.tags) ?? []);
|
||||
|
||||
if (featured) {
|
||||
return {
|
||||
templates: templates.filter((t) => t.featured),
|
||||
tags: allTags,
|
||||
};
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
return {
|
||||
templates: templates.filter((t) => t.tags.includes(tag)),
|
||||
tags: allTags,
|
||||
};
|
||||
}
|
||||
|
||||
return { templates, tags: allTags };
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { employeeDb } from './templates/employee-db';
|
||||
import { visualNovelDb } from './templates/visual-novel-db';
|
||||
import { airbnbDb } from './templates/airbnb-db';
|
||||
import { wordpressDb } from './templates/wordpress-db';
|
||||
import { pokemonDb } from './templates/pokemon-db';
|
||||
|
||||
export interface Template {
|
||||
slug: string;
|
||||
@@ -11,9 +14,14 @@ export interface Template {
|
||||
imageDark: string;
|
||||
diagram: Diagram;
|
||||
tags: string[];
|
||||
keywords: string[];
|
||||
featured: boolean;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export const templates: Template[] = [employeeDb, visualNovelDb];
|
||||
export const templates: Template[] = [
|
||||
employeeDb,
|
||||
visualNovelDb,
|
||||
airbnbDb,
|
||||
wordpressDb,
|
||||
pokemonDb,
|
||||
];
|
||||
|
||||
1785
src/templates-data/templates/airbnb-db.ts
Normal file
@@ -5,28 +5,16 @@ import imageDark from '@/assets/templates/employeedb-dark.png';
|
||||
|
||||
export const employeeDb: Template = {
|
||||
slug: 'employees-db',
|
||||
name: 'Employees schema',
|
||||
name: 'Employees',
|
||||
shortDescription: 'Employees, departments, and salaries',
|
||||
description:
|
||||
'A schema for database of employees, departments, and salaries.',
|
||||
image,
|
||||
imageDark,
|
||||
tags: ['mysql'],
|
||||
keywords: [
|
||||
'Employees database schema',
|
||||
'Employees database template',
|
||||
'database schema visualization',
|
||||
'Employees database design',
|
||||
'ChartDB',
|
||||
'Employees schema diagram',
|
||||
'relational database structure',
|
||||
'Employees development',
|
||||
'Employees tables',
|
||||
'database template for Employees',
|
||||
],
|
||||
featured: true,
|
||||
diagram: {
|
||||
id: 'diagramexample01',
|
||||
id: 'employees_db',
|
||||
name: 'employees-db',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
|
||||
1879
src/templates-data/templates/pokemon-db.ts
Normal file
@@ -5,25 +5,13 @@ import imageDark from '@/assets/templates/visual-novel-db-dark.png';
|
||||
|
||||
export const visualNovelDb: Template = {
|
||||
slug: 'visual-novel-db',
|
||||
name: 'The Visual Novel Database | vndb',
|
||||
shortDescription: 'The Visual Novel Database | vndb',
|
||||
description: 'A comprehensive database for information about visual novels',
|
||||
name: 'The Visual Novel Database',
|
||||
shortDescription: 'The Visual Novel Database',
|
||||
description:
|
||||
'A comprehensive database for information about visual novels.',
|
||||
image,
|
||||
imageDark,
|
||||
tags: ['postgres'],
|
||||
keywords: [
|
||||
'VNDB',
|
||||
'visual novel database schema',
|
||||
'visual novel database template',
|
||||
'database schema visualization',
|
||||
'visual novel database design',
|
||||
'ChartDB',
|
||||
'VNDB schema diagram',
|
||||
'relational database structure',
|
||||
'VNDB development',
|
||||
'VNDB tables',
|
||||
'database template for VNDB',
|
||||
],
|
||||
featured: true,
|
||||
url: 'https://vndb.org',
|
||||
diagram: {
|
||||
|
||||