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 ` ); }; 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) => ( diff --git a/src/boats/pages/settings/access.tsx b/src/boats/pages/settings/access.tsx index 15bccdd..7e3d4de 100644 --- a/src/boats/pages/settings/access.tsx +++ b/src/boats/pages/settings/access.tsx @@ -1,10 +1,10 @@ -import { useTranslation } from '@refinedev/core'; +import { useTranslate } from '@refinedev/core'; import { AccessList } from '@/boats/components/access-list'; import { SettingsLayout } from '@/boats/components/settings-layout'; const AccessSettings = () => { - const { translate } = useTranslation(); + const translate = useTranslate(); return ( diff --git a/src/boats/pages/settings/common.tsx b/src/boats/pages/settings/common.tsx index 1b6f67c..c16e3ae 100644 --- a/src/boats/pages/settings/common.tsx +++ b/src/boats/pages/settings/common.tsx @@ -1,29 +1,73 @@ -import { useTranslation } from '@refinedev/core'; -import { Button } from 'antd'; +import { useTranslate, useUpdate } from '@refinedev/core'; +import { Button, Col, Form, Input, Row } from 'antd'; +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router'; +import { BoatDeleteButton } from '@/boats/components/boat-delete-button'; import { SettingsLayout } from '@/boats/components/settings-layout'; +import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; import { SectionHeader } from '@/shared/components/section-header'; const CommonSettings = () => { - const { translate } = useTranslation(); + const translate = useTranslate(); + const { boatId } = useParams(); + const { data, isLoading } = useCurrentBoat(); + const [name, setName] = useState(''); + const { mutate, isLoading: isUpdating } = useUpdate(); + + useEffect(() => { + if (data?.data?.name) { + setName(data.data.name); + } + }, [data]); + + const handleSave = () => { + if (!name.trim()) return; + mutate({ + resource: 'boats', + id: boatId, + values: { name }, + successNotification: () => ({ + message: translate('settings.common.rename.success'), + type: 'success', + }), + errorNotification: () => ({ + message: translate('settings.common.rename.error'), + type: 'error', + }), + }); + }; return ( -
- - -
+
+ +
+ + setName(e.target.value)} + disabled={isLoading || isUpdating} + /> + + + + + + + + + ); }; diff --git a/src/core/components/app-provider.tsx b/src/core/components/app-provider.tsx index 3282cd0..0f5e771 100644 --- a/src/core/components/app-provider.tsx +++ b/src/core/components/app-provider.tsx @@ -1,3 +1,4 @@ +import { useNotificationProvider } from '@refinedev/antd'; import { Refine } from '@refinedev/core'; import routerProvider from '@refinedev/react-router'; import { dataProvider } from '@refinedev/supabase'; @@ -27,6 +28,7 @@ const AppProvider: FC = ({ children }) => { authProvider={authProvider} routerProvider={routerProvider} i18nProvider={i18nProvider} + notificationProvider={useNotificationProvider} > {children} diff --git a/src/equipments/components/equipment-actions-menu.tsx b/src/equipments/components/equipment-actions-menu.tsx index fb7e815..3e18fe0 100644 --- a/src/equipments/components/equipment-actions-menu.tsx +++ b/src/equipments/components/equipment-actions-menu.tsx @@ -1,5 +1,5 @@ import { DeleteOutlined, EditOutlined, MoreOutlined } from '@ant-design/icons'; -import { useDelete, useGo, useTranslation } from '@refinedev/core'; +import { useDelete, useGo, useTranslate } from '@refinedev/core'; import { Button, Dropdown, type MenuProps, Modal } from 'antd'; import type { FC } from 'react'; @@ -13,7 +13,7 @@ interface EquipmentActionsMenuProps { export const EquipmentActionsMenu: FC = ({ equipment, }) => { - const { translate } = useTranslation(); + const translate = useTranslate(); const { data: boat } = useCurrentBoat(); const go = useGo(); diff --git a/src/equipments/components/equipment-attachment-list.tsx b/src/equipments/components/equipment-attachment-list.tsx index 8e5fcaa..155e85e 100644 --- a/src/equipments/components/equipment-attachment-list.tsx +++ b/src/equipments/components/equipment-attachment-list.tsx @@ -3,7 +3,7 @@ import { DownloadOutlined, PaperClipOutlined, } from '@ant-design/icons'; -import { useCreate, useDelete, useList, useTranslation } from '@refinedev/core'; +import { useCreate, useDelete, useList, useTranslate } from '@refinedev/core'; import { Button, Card, @@ -27,7 +27,7 @@ type EquipmentAttachmentListProps = { const EquipmentAttachmentList: FC = ({ equipmentId, }) => { - const { translate } = useTranslation(); + const translate = useTranslate(); const { data: boat } = useCurrentBoat(); const { data: attachments } = useList({ diff --git a/src/equipments/components/equipment-form.tsx b/src/equipments/components/equipment-form.tsx index 6d0259d..bab1b44 100644 --- a/src/equipments/components/equipment-form.tsx +++ b/src/equipments/components/equipment-form.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from '@refinedev/core'; +import { useTranslate } from '@refinedev/core'; import { DatePicker, Form, Input, InputNumber } from 'antd'; import dayjs from 'dayjs'; import type { FC } from 'react'; @@ -14,7 +14,7 @@ const EquipmentForm: FC = ({ formProps, handleOnFinish, }) => { - const { translate } = useTranslation(); + const translate = useTranslate(); return (
diff --git a/src/equipments/pages/add.tsx b/src/equipments/pages/add.tsx index 5f34c96..2314403 100644 --- a/src/equipments/pages/add.tsx +++ b/src/equipments/pages/add.tsx @@ -1,5 +1,5 @@ import { Create, useForm } from '@refinedev/antd'; -import { useGo, useTranslation } from '@refinedev/core'; +import { useGo, useTranslate } from '@refinedev/core'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; import { EquipmentForm } from '@/equipments/components/equipment-form'; @@ -11,7 +11,7 @@ interface EquipmentFormValues { const AddEquipment = () => { const { data: boat } = useCurrentBoat(); - const { translate } = useTranslation(); + const translate = useTranslate(); const go = useGo(); const { formProps, saveButtonProps, onFinish } = useForm({ diff --git a/src/equipments/pages/edit.tsx b/src/equipments/pages/edit.tsx index 0f3c03a..d9176a9 100644 --- a/src/equipments/pages/edit.tsx +++ b/src/equipments/pages/edit.tsx @@ -1,5 +1,5 @@ import { Edit, useForm } from '@refinedev/antd'; -import { useTranslation } from '@refinedev/core'; +import { useTranslate } from '@refinedev/core'; import { useParams } from 'react-router'; import { EquipmentForm } from '@/equipments/components/equipment-form'; @@ -7,7 +7,7 @@ import type { UpdateEquipment } from '@/shared/types/models'; const EditEquipment = () => { const { equipmentId } = useParams<{ equipmentId: string }>(); - const { translate } = useTranslation(); + const translate = useTranslate(); const { formProps, saveButtonProps, onFinish } = useForm({ resource: 'equipments', action: 'edit', diff --git a/src/equipments/pages/list.tsx b/src/equipments/pages/list.tsx index d33ada8..7414fc5 100644 --- a/src/equipments/pages/list.tsx +++ b/src/equipments/pages/list.tsx @@ -1,4 +1,4 @@ -import { Link, useInfiniteList, useTranslation } from '@refinedev/core'; +import { Link, useInfiniteList, useTranslate } from '@refinedev/core'; import { Button, Card, Empty, List } from 'antd'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; @@ -26,7 +26,7 @@ const EquipmentList = () => { } = useInfiniteList({ resource: 'equipments', }); - const { translate } = useTranslation(); + const translate = useTranslate(); const groupedEquipments: Record = equipments?.pages diff --git a/src/equipments/pages/show.tsx b/src/equipments/pages/show.tsx index 4af853c..93f6f7b 100644 --- a/src/equipments/pages/show.tsx +++ b/src/equipments/pages/show.tsx @@ -1,4 +1,4 @@ -import { Link, useOne, useTranslation } from '@refinedev/core'; +import { Link, useOne, useTranslate } from '@refinedev/core'; import { Button, Card, Typography } from 'antd'; import dayjs from 'dayjs'; import { useParams } from 'react-router'; @@ -15,7 +15,7 @@ const ShowEquipment = () => { id: equipmentId, }); - const { translate } = useTranslation(); + const translate = useTranslate(); return ( <> diff --git a/src/interventions/components/intervention-form.tsx b/src/interventions/components/intervention-form.tsx index 94cd10b..5fea87f 100644 --- a/src/interventions/components/intervention-form.tsx +++ b/src/interventions/components/intervention-form.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from '@refinedev/core'; +import { useTranslate } from '@refinedev/core'; import { DatePicker, Form, Input } from 'antd'; import dayjs from 'dayjs'; import type { FC } from 'react'; @@ -12,21 +12,24 @@ const InterventionForm: FC = ({ formProps, handleOnFinish, }) => { - const { translate } = useTranslation(); + const translate = useTranslate(); return ( - + ({ value: value ? dayjs(value) : null })} > diff --git a/src/interventions/pages/add.tsx b/src/interventions/pages/add.tsx index 6fe116e..4a38457 100644 --- a/src/interventions/pages/add.tsx +++ b/src/interventions/pages/add.tsx @@ -1,5 +1,5 @@ import { Create, useForm } from '@refinedev/antd'; -import { useTranslation } from '@refinedev/core'; +import { useTranslate } from '@refinedev/core'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; import { InterventionForm } from '@/interventions/components/intervention-form'; @@ -10,7 +10,7 @@ interface InterventionFormValues { const AddIntervention = () => { const { data: boat } = useCurrentBoat(); - const { translate } = useTranslation(); + const translate = useTranslate(); const { formProps, saveButtonProps, onFinish } = useForm({ resource: 'interventions', @@ -27,7 +27,7 @@ const AddIntervention = () => { return ( diff --git a/src/interventions/pages/edit.tsx b/src/interventions/pages/edit.tsx index 1cbfc5e..fc22898 100644 --- a/src/interventions/pages/edit.tsx +++ b/src/interventions/pages/edit.tsx @@ -1,4 +1,5 @@ import { Edit, useForm } from '@refinedev/antd'; +import { useTranslate } from '@refinedev/core'; import { useParams } from 'react-router'; import { InterventionForm } from '@/interventions/components/intervention-form'; @@ -11,9 +12,13 @@ const EditIntervention = () => { action: 'edit', id: interventionId, }); + const translate = useTranslate(); return ( - + ); diff --git a/src/interventions/pages/list.tsx b/src/interventions/pages/list.tsx index 3358810..9578969 100644 --- a/src/interventions/pages/list.tsx +++ b/src/interventions/pages/list.tsx @@ -1,4 +1,4 @@ -import { Link, useList, useTranslation } from '@refinedev/core'; +import { Link, useList, useTranslate } from '@refinedev/core'; import { Button, List } from 'antd'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; @@ -11,15 +11,17 @@ const InterventionList = () => { resource: 'interventions', }); - const { translate } = useTranslation(); + const translate = useTranslate(); return ( <> - + } /> diff --git a/src/interventions/pages/show.tsx b/src/interventions/pages/show.tsx index 866f10e..9ecf4e7 100644 --- a/src/interventions/pages/show.tsx +++ b/src/interventions/pages/show.tsx @@ -1,4 +1,4 @@ -import { Link, useOne, useTranslation } from '@refinedev/core'; +import { Link, useOne, useTranslate } from '@refinedev/core'; import { Button, Typography } from 'antd'; import { useParams } from 'react-router'; @@ -14,7 +14,7 @@ const ShowIntervention = () => { id: interventionId as string, }); - const { translate } = useTranslation(); + const translate = useTranslate(); return ( <> @@ -24,7 +24,9 @@ const ShowIntervention = () => { - + } />