diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..0f311ab
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,66 @@
+# VesselVigil — AI Agent Instructions
+
+This document guides AI agents to contribute effectively to the VesselVigil project.
+
+## Overview
+- **VesselVigil** is an open-source application for managing boat maintenance: tracking interventions, inventory of parts, access and account management.
+- Architecture:
+ - Frontend : **React + TypeScript** with **Vite**
+ - Backend : **Supabase** for database and authentication.
+- Folders structure:
+ - The project uses a **feature-based architecture**. This improves modularity and makes it easier to work on a specific feature without affecting others.
+ - `src/boats/`, `src/equipments/`, `src/interventions/`: each main business domain (boats, equipments, interventions) is organized in its own folder containing all related components, pages, hooks, and utils.
+ - `src/core/`: layout, provider, global routing.
+ - `src/shared/`: reusable components and common types.
+ - `supabase/`: SQL schemas, migrations, seeds.
+
+## Conventions and patterns
+- **Pages**: each entity has a `pages/` folder for main views (e.g., `add.tsx`, `list.tsx`, `dashboard.tsx`).
+- **Components**: reusable, organized by entity in `components/`.
+- **Hooks**: business logic in `hooks/` (e.g., `use-current-boat.tsx`).
+- **Utils**: business helpers in `utils/`.
+- **Types**: centralized in `src/shared/types/`.
+- **I18n**: translation files in `public/locales/`.
+- **Authentication**: handled via Supabase and Refine, see `src/auth/providers/auth-provider.ts`.
+
+## React librairies
+- **Refine**: used for data management and UI components.
+- **Ant Design**: UI components library.
+- **React Router**: for routing.
+
+## React usage
+- Use **functional components** with hooks.
+- Use **TypeScript** for type safety.
+- Use kebab-case for file names (e.g., `add-boat.tsx`, `boat-list.tsx`).
+- Use **named exports** for components.
+- Export components at the bottom of the file.
+- Use PascalCase for component names (e.g., `AddBoat`, `BoatList`).
+- One component per file, with the file name matching the component name.
+
+## Supabase usage
+- **Supabase client**: Always access Supabase using the helper in `src/core/utils/supabaseClient.ts`. Do not instantiate Supabase clients elsewhere.
+- **Database schemas**: All table and relationship definitions are stored in `supabase/schemas/`. Update these files when changing the database structure.
+- **Migrations**: SQL migration scripts are located in `supabase/migrations/`. These scripts are generated from the schema files and must be applied to keep the Supabase database up to date.
+
+## Refine usage
+- Use Refine hooks (e.g., `useList`, `useDelete`, `useCreate`) for all Supabase data operations. Avoid direct Supabase client calls except in utilities.
+- Organize Refine logic by feature: hooks and pages for each entity (boats, equipments, interventions).
+- Prefer declarative data fetching and mutation via Refine, and leverage its built-in error/loading states.
+- Example: To list boats, use `useList` in `src/boats/pages/list.tsx`.
+- To navigate between pages, use the `useGo` hook or the `Link` component from Refine.
+
+## Ant Design usage
+- Use Ant Design components for all UI elements unless a custom component is required.
+- Customize Ant Design components using props and the project's global styles (`src/global.css`).
+- Organize UI components by feature in their respective `components/` folders.
+- Example: Use `
` for entity lists, `` for add/edit pages, and `` for actions.
+
+## Translations usage
+- All translations are stored in `public/locales/` in JSON files.
+- Use `translate` function from `useTranslate` hook from Refine to access translations in components.
+- Always provide translations for both English and French.
+- Split each part of key into a json object inside locale files, e.g., `common.deleteBoat.button` will be `{ "common": { "deleteBoat": { "button": "The translation for the key" }}}`.
+- Refine use i18next under the hood, follow its conventions for pluralization and interpolation.
+
+## Project-specific best practices
+- To get information about the current boat, use the `useCurrentBoat` hook from `src/boats/hooks/use-current-boat.tsx`.
diff --git a/public/locales/en.json b/public/locales/en.json
index 79a0be4..ad131ad 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -76,46 +76,46 @@
"buttons": {
"submit": "Update"
}
- },
- "dashboard": {
- "title": "Tableau de bord"
- }
- },
- "LogoutButton": {
- "label": "Logout"
- },
- "ListBoat": {
- "title": "Your boats",
- "add": "Create a new boat"
- },
- "AddBoat": {
- "title": "Add a new boat",
- "labels": {
- "name": "Name"
- }
- },
- "InterventionList": {
- "title": "Intervention List",
- "add": "Add an intervention"
- },
- "AddIntervention": {
- "title": "Add an intervention",
- "labels": {
- "title": "Title",
- "date": "Date",
- "description": "Description"
}
},
- "InterventionShow": {
- "edit": "Edit Intervention"
+ "auth": {
+ "logout": "Logout"
},
"boats": {
+ "list": {
+ "title": "Your boats",
+ "add": "Create a new boat"
+ },
+ "add": {
+ "title": "Add a new boat",
+ "notification": {
+ "success": {
+ "message": "Your boat has been created.",
+ "description": "Success"
+ },
+ "error": {
+ "message": "There was an error while creating your boat.",
+ "description": "Error"
+ }
+ },
+ "form": {
+ "labels": {
+ "name": "Name"
+ },
+ "validation": {
+ "name_required": "Boat name is required."
+ }
+ }
+ },
"menu": {
"dashboard": "Dashboard",
"interventions": "Interventions",
"equipments": "Equipments",
"settings": "Settings"
},
+ "dashboard": {
+ "title": "Dashboard"
+ },
"systems": {
"select": {
"placeholder": "Select a system"
@@ -207,6 +207,28 @@
"add": "Add an attachment"
}
},
+ "interventions": {
+ "list": {
+ "title": "Intervention List",
+ "add": "Add an intervention"
+ },
+ "add": {
+ "title": "Add an intervention"
+ },
+ "show": {
+ "edit": "Edit Intervention"
+ },
+ "edit": {
+ "title": "Edit Intervention"
+ },
+ "form": {
+ "labels": {
+ "title": "Title",
+ "date": "Date",
+ "description": "Description"
+ }
+ }
+ },
"settings": {
"menu": {
"common": "General",
@@ -214,9 +236,25 @@
},
"common": {
"title": "General Settings",
+ "rename": {
+ "label": "Boat name",
+ "save": "Rename",
+ "success": "Boat name updated successfully.",
+ "error": "Error updating boat name."
+ },
"dangerZone": {
"title": "Danger Zone",
"description": "Danger zone Be wary of the following features as they cannot be undone."
+ },
+ "deleteBoat": {
+ "button": "Delete boat",
+ "modalTitle": "Confirm deletion",
+ "modalText": "To confirm deletion, please type the name of the boat:",
+ "inputPlaceholder": "Boat name",
+ "okText": "Delete",
+ "cancelText": "Cancel",
+ "success": "Boat deleted",
+ "error": "Error while deleting"
}
},
"access": {
diff --git a/public/locales/fr.json b/public/locales/fr.json
index 3f98e9a..9117f3d 100644
--- a/public/locales/fr.json
+++ b/public/locales/fr.json
@@ -76,46 +76,46 @@
"buttons": {
"submit": "Mettre à jour"
}
- },
- "dashboard": {
- "title": "Tableau de bord"
}
},
- "LogoutButton": {
- "label": "Déconnexion"
- },
- "ListBoat": {
- "title": "Vos bateaux",
- "add": "Ajouter un nouveau bateau"
- },
- "AddBoat": {
- "title": "Ajouter un nouveau bateau",
- "labels": {
- "name": "Nom"
- }
- },
- "InterventionList": {
- "title": "Liste des interventions",
- "add": "Ajouter une intervention"
- },
- "AddIntervention": {
- "title": "Ajouter une intervention",
- "labels": {
- "title": "Titre",
- "date": "Date",
- "description": "Description"
- }
- },
- "InterventionShow": {
- "edit": "Modifier l'intervention"
+ "auth": {
+ "logout": "Déconnexion"
},
"boats": {
+ "list": {
+ "title": "Vos bateaux",
+ "add": "Ajouter un nouveau bateau"
+ },
+ "add": {
+ "title": "Ajouter un nouveau bateau",
+ "notification": {
+ "success": {
+ "description": "Succès",
+ "message": "Votre bateau a été créé avec succès."
+ },
+ "error": {
+ "description": "Une erreur est survenue lors de la création de votre bateau.",
+ "message": "Erreur"
+ }
+ },
+ "form": {
+ "labels": {
+ "name": "Nom"
+ },
+ "validation": {
+ "name_required": "Le nom du bateau est requis."
+ }
+ }
+ },
"menu": {
"dashboard": "Tableau de bord",
"interventions": "Interventions",
"equipments": "Équipements",
"settings": "Paramètres"
},
+ "dashboard": {
+ "title": "Tableau de bord"
+ },
"systems": {
"select": {
"placeholder": "Sélectionnez un système"
@@ -174,13 +174,13 @@
"add": {
"title": "Ajouter un équipement"
},
+ "edit": {
+ "title": "Modifier l'équipement"
+ },
"show": {
"title": "Détails de l'équipement",
"edit": "Modifier l'équipement"
},
- "edit": {
- "title": "Modifier l'équipement"
- },
"delete": {
"confirmTitle": "Confirmer la suppression",
"confirmContent": "Voulez-vous vraiment supprimer cet équipement ?"
@@ -207,6 +207,28 @@
"add": "Ajouter une pièce jointe"
}
},
+ "interventions": {
+ "list": {
+ "title": "Liste des interventions",
+ "add": "Ajouter une intervention"
+ },
+ "add": {
+ "title": "Ajouter une intervention"
+ },
+ "show": {
+ "edit": "Modifier l'intervention"
+ },
+ "edit": {
+ "title": "Editer l'intervention"
+ },
+ "form": {
+ "labels": {
+ "title": "Titre",
+ "date": "Date",
+ "description": "Description"
+ }
+ }
+ },
"settings": {
"menu": {
"common": "Général",
@@ -214,9 +236,25 @@
},
"common": {
"title": "Paramètres généraux",
+ "rename": {
+ "label": "Nom du bateau",
+ "save": "Renommer",
+ "success": "Nom du bateau mis à jour avec succès.",
+ "error": "Erreur lors de la mise à jour du nom du bateau."
+ },
"dangerZone": {
"title": "Zone de danger",
"description": "Zone de danger : Faites attention aux fonctionnalités suivantes car elles sont irréversibles."
+ },
+ "deleteBoat": {
+ "button": "Supprimer le bateau",
+ "modalTitle": "Confirmer la suppression",
+ "modalText": "Pour confirmer la suppression, veuillez saisir le nom du bateau :",
+ "inputPlaceholder": "Nom du bateau",
+ "okText": "Supprimer",
+ "cancelText": "Annuler",
+ "success": "Bateau supprimé",
+ "error": "Erreur lors de la suppression"
}
},
"access": {
diff --git a/src/auth/components/logout-button.tsx b/src/auth/components/logout-button.tsx
index e1d3100..0b3415c 100644
--- a/src/auth/components/logout-button.tsx
+++ b/src/auth/components/logout-button.tsx
@@ -1,6 +1,5 @@
-import { useLogout } from '@refinedev/core';
+import { useLogout, useTranslate } from '@refinedev/core';
import type { FC } from 'react';
-import { useTranslation } from '@refinedev/core';
interface LogoutButtonProps {
style?: React.CSSProperties;
@@ -9,11 +8,11 @@ interface LogoutButtonProps {
export const LogoutButton: FC = ({ style }) => {
const { mutate: logout } = useLogout();
- const { translate } = useTranslation();
+ const translate = useTranslate();
return (
);
};
diff --git a/src/auth/pages/login.tsx b/src/auth/pages/login.tsx
index f3b77d0..2ab0f3c 100644
--- a/src/auth/pages/login.tsx
+++ b/src/auth/pages/login.tsx
@@ -1,9 +1,9 @@
import { GoogleOutlined } from '@ant-design/icons';
import { AuthPage } from '@refinedev/antd';
-import { useTranslation } from '@refinedev/core';
+import { useTranslate } from '@refinedev/core';
const Login = () => {
- const { translate } = useTranslation();
+ const translate = useTranslate();
return (
{
const {
@@ -15,7 +14,7 @@ const AccessList = () => {
resource: 'accesses',
});
- const { translate } = useTranslation();
+ const translate = useTranslate();
const [isModalOpen, setIsModalOpen] = useState(false);
diff --git a/src/boats/components/boat-delete-button.tsx b/src/boats/components/boat-delete-button.tsx
new file mode 100644
index 0000000..41ea979
--- /dev/null
+++ b/src/boats/components/boat-delete-button.tsx
@@ -0,0 +1,74 @@
+import { useDelete, useGo, useTranslate } from '@refinedev/core';
+import { Button, Input, Modal, Typography } from 'antd';
+import { useState } from 'react';
+
+import { useCurrentBoat } from '@/boats/hooks/use-current-boat';
+
+export const BoatDeleteButton = () => {
+ const { data } = useCurrentBoat();
+ const boat = data?.data;
+ const boatId = boat?.id;
+ const boatName = boat?.name;
+ const [visible, setVisible] = useState(false);
+ const [inputName, setInputName] = useState('');
+ const { mutate, isLoading } = useDelete({});
+ const go = useGo();
+ const translate = useTranslate();
+
+ const showModal = () => setVisible(true);
+ const handleCancel = () => {
+ setVisible(false);
+ setInputName('');
+ };
+ const handleDelete = () => {
+ if (!boatId) return;
+ mutate(
+ {
+ resource: 'boats',
+ id: boatId,
+ successNotification: () => ({
+ message: translate('settings.common.deleteBoat.success'),
+ type: 'success',
+ }),
+ errorNotification: () => ({
+ message: translate('settings.common.deleteBoat.error'),
+ type: 'error',
+ }),
+ },
+ {
+ onSuccess: () => {
+ go({ to: '/boats', type: 'replace' });
+ },
+ },
+ );
+ };
+
+ return (
+ <>
+
+
+
+ {`${translate('settings.common.deleteBoat.modalText')} `}
+ {boatName}
+
+ setInputName(e.target.value)}
+ />
+
+ >
+ );
+};
diff --git a/src/boats/components/boat-menu.tsx b/src/boats/components/boat-menu.tsx
index 7cbe874..7625d40 100644
--- a/src/boats/components/boat-menu.tsx
+++ b/src/boats/components/boat-menu.tsx
@@ -1,4 +1,4 @@
-import { useGo, useTranslation } from '@refinedev/core';
+import { useGo, useTranslate } from '@refinedev/core';
import type { MenuProps } from 'antd';
import { Grid, Menu } from 'antd';
import { useLocation } from 'react-router';
@@ -25,7 +25,7 @@ const { useBreakpoint } = Grid;
const BoatMenu = () => {
const { pathname } = useLocation();
const go = useGo();
- const { translate } = useTranslation();
+ const translate = useTranslate();
const screens = useBreakpoint();
const keys = pathname.split('/').filter(Boolean);
diff --git a/src/boats/components/boat-system-select.tsx b/src/boats/components/boat-system-select.tsx
index 809d54e..b150190 100644
--- a/src/boats/components/boat-system-select.tsx
+++ b/src/boats/components/boat-system-select.tsx
@@ -1,4 +1,4 @@
-import { useTranslation } from '@refinedev/core';
+import { useTranslate } from '@refinedev/core';
import { Select } from 'antd';
import type { FC } from 'react';
@@ -11,7 +11,7 @@ interface BoatSystemSelectProps {
}
const BoatSystemSelect: FC = ({ value, onChange }) => {
- const { translate } = useTranslation();
+ const translate = useTranslate();
const options = boatSystemList.map((key) => ({
value: key,
diff --git a/src/boats/components/settings-menu.tsx b/src/boats/components/settings-menu.tsx
index b313825..96a4bd5 100644
--- a/src/boats/components/settings-menu.tsx
+++ b/src/boats/components/settings-menu.tsx
@@ -1,4 +1,4 @@
-import { useGo, useTranslation } from '@refinedev/core';
+import { useGo, useTranslate } from '@refinedev/core';
import { Menu } from 'antd';
import { useLocation } from 'react-router';
@@ -16,7 +16,7 @@ const items: MenuItem[] = [
const SettingsMenu = () => {
const { pathname } = useLocation();
const go = useGo();
- const { translate } = useTranslation();
+ const translate = useTranslate();
const keys = pathname.split('/').filter(Boolean);
diff --git a/src/boats/pages/add.tsx b/src/boats/pages/add.tsx
index e9cc627..f38703c 100644
--- a/src/boats/pages/add.tsx
+++ b/src/boats/pages/add.tsx
@@ -1,34 +1,58 @@
import { Create, useForm } from '@refinedev/antd';
-import { useGetIdentity, useTranslation } from '@refinedev/core';
+import { useGetIdentity, useGo, useTranslate } from '@refinedev/core';
import { Form, Input } from 'antd';
import { PageLayout } from '@/shared/components/page-layout';
const AddBoat = () => {
const { data: identity } = useGetIdentity<{ id: string }>();
+ const go = useGo();
const { formProps, saveButtonProps, onFinish } = useForm({
resource: 'boats',
action: 'create',
+ redirect: false,
+ successNotification: () => ({
+ type: 'success',
+ message: translate('boats.add.notification.success.message'),
+ description: translate('boats.add.notification.success.description'),
+ }),
+ errorNotification: () => ({
+ type: 'error',
+ message: translate('boats.add.notification.error.message'),
+ description: translate('boats.add.notification.error.description'),
+ }),
+ onMutationSuccess: () => {
+ go({ to: '/boats', type: 'replace' });
+ },
});
- const handleOnFinish = (values: {}) => {
+ const handleOnFinish = (values: Record) => {
onFinish({
created_by: identity?.id,
...values,
});
};
- const { translate } = useTranslation();
+ const translate = useTranslate();
return (
+
diff --git a/src/boats/pages/dashboard.tsx b/src/boats/pages/dashboard.tsx
index bb6cdbb..a0baa8b 100644
--- a/src/boats/pages/dashboard.tsx
+++ b/src/boats/pages/dashboard.tsx
@@ -1,10 +1,10 @@
-import { useTranslation } from '@refinedev/core';
+import { useTranslate } from '@refinedev/core';
import { PageHeader } from '@/shared/components/page-header';
const BoatDashboard = () => {
- const { translate } = useTranslation();
- return ;
+ const translate = useTranslate();
+ return ;
};
export { BoatDashboard };
diff --git a/src/boats/pages/list.tsx b/src/boats/pages/list.tsx
index a62248b..ec99b38 100644
--- a/src/boats/pages/list.tsx
+++ b/src/boats/pages/list.tsx
@@ -1,5 +1,5 @@
import { PlusOutlined } from '@ant-design/icons';
-import { Link, useList, useTranslation } from '@refinedev/core';
+import { Link, useList, useTranslate } from '@refinedev/core';
import { Card, Col, Row } from 'antd';
import { PageHeader } from '@/shared/components/page-header';
@@ -10,11 +10,11 @@ const ListBoat = () => {
resource: 'boats',
});
- const { translate } = useTranslation();
+ const translate = useTranslate();
return (
-
+
{boats?.data?.map((boat) => (