Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ This document guides AI agents to contribute effectively to the VesselVigil proj
- **I18n**: translation files in `public/locales/`.
- **Authentication**: handled via Supabase and Refine, see `src/auth/providers/auth-provider.ts`.

## Commit messages
- Use the conventional commits format: `type(scope): Description`.
- Types include: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`.
- Use the imperative mood in the description (e.g., `feat: Add new boat management feature`).
- Include a short description of the change and, if necessary, a longer explanation in the body.
- Example: `feat(boats): Add boat management page with list and add functionality`.
- Always write in English

## React librairies
- **Refine**: used for data management and UI components.
- **Ant Design**: UI components library.
Expand Down
12 changes: 10 additions & 2 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,16 @@
},
"shared": {
"attachments": {
"title": "Attachments",
"add": "Add an attachment",
"photo": {
"title": "Photos",
"empty": "No photos found",
"add": "Add photo"
},
"document": {
"title": "Documents",
"empty": "No documents found",
"add": "Add document"
},
"download": "Download",
"delete": "Delete"
},
Expand Down
12 changes: 10 additions & 2 deletions public/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,16 @@
},
"shared": {
"attachments": {
"title": "Pièces jointes",
"add": "Ajouter une pièce jointe",
"photo": {
"title": "Photos",
"empty": "Aucune photo trouvée",
"add": "Ajouter une photo"
},
"document": {
"title": "Documents",
"empty": "Aucun document trouvé",
"add": "Ajouter un document"
},
"download": "Télécharger",
"delete": "Supprimer"
},
Expand Down
11 changes: 10 additions & 1 deletion src/equipments/pages/show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,16 @@ const ShowEquipment = () => {
</Typography.Paragraph>
) : null}
</Card>
<AttachmentList resource="equipment" resourceId={equipmentId} />
<AttachmentList
resource="equipment"
resourceId={equipmentId}
type="photo"
/>
<AttachmentList
resource="equipment"
resourceId={equipmentId}
type="document"
/>
</>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/interventions/pages/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const InterventionList = () => {
useInfiniteList<Intervention>({
resource: 'interventions',
pagination: { pageSize: 50 },
sorters: [{ field: 'id', order: 'desc' }],
sorters: [{ field: 'date', order: 'desc' }],
Comment thread
cballevre marked this conversation as resolved.
});

const { getLocale, translate } = useTranslation();
Expand Down
11 changes: 10 additions & 1 deletion src/interventions/pages/show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,16 @@ const ShowIntervention = () => {
</>
) : null}
</Card>
<AttachmentList resource="intervention" resourceId={interventionId} />
<AttachmentList
resource="intervention"
resourceId={interventionId}
type="photo"
/>
<AttachmentList
resource="intervention"
resourceId={interventionId}
type="document"
/>
</>
);
};
Expand Down
58 changes: 50 additions & 8 deletions src/shared/components/attachment-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,35 @@ import {
PaperClipOutlined,
} from '@ant-design/icons';
import { useCreate, useDelete, useList, useTranslate } from '@refinedev/core';
import { Button, Card, Col, List, Row, Upload, type UploadProps } from 'antd';
import {
Button,
Card,
Col,
Empty,
List,
Row,
Upload,
type UploadProps,
} from 'antd';
import type { FC } from 'react';

import { useCurrentBoat } from '@/boats/hooks/use-current-boat';
import { supabaseClient as supabase } from '@/core/utils/supabaseClient';
import { SectionHeader } from '@/shared/components/section-header';
import type { EquipmentAttachment } from '@/shared/types/models';
import { sanitizeFileName } from '@/shared/utils/sanitize-file-name';

export type AttachmentListProps = {
resource: string;
resource: 'intervention' | 'equipment';
resourceId?: string;
type: 'photo' | 'document';
};

const AttachmentList: FC<AttachmentListProps> = ({ resource, resourceId }) => {
const AttachmentList: FC<AttachmentListProps> = ({
resource,
resourceId,
type,
}) => {
const translate = useTranslate();
const { data: boat } = useCurrentBoat();

Expand All @@ -27,7 +42,10 @@ const AttachmentList: FC<AttachmentListProps> = ({ resource, resourceId }) => {

const { data: attachments } = useList<EquipmentAttachment>({
resource: attachmentResource,
filters: [{ field: resourceForeignKey, operator: 'eq', value: resourceId }],
filters: [
{ field: resourceForeignKey, operator: 'eq', value: resourceId },
{ field: 'type', operator: 'eq', value: type },
],
});

const { mutate: createAttachment } = useCreate({
Expand All @@ -43,7 +61,8 @@ const AttachmentList: FC<AttachmentListProps> = ({ resource, resourceId }) => {
}) => {
try {
const uploadedFile = file as File;
const filePath = `${boat?.data?.id}/${resource}s/${resourceId}/attachments/${Date.now()}_${uploadedFile.name}`;
const safeName = sanitizeFileName(uploadedFile.name);
const filePath = `${boat?.data?.id}/${resource}s/${resourceId}/attachments/${Date.now()}_${safeName}`;

const { error: uploadError } = await supabase.storage
.from(boatAttachmentBucket)
Expand All @@ -62,6 +81,7 @@ const AttachmentList: FC<AttachmentListProps> = ({ resource, resourceId }) => {
file_name: uploadedFile.name,
file_path: filePath,
file_type: uploadedFile.type,
type,
},
});
onSuccess?.('File uploaded successfully!');
Expand Down Expand Up @@ -107,9 +127,31 @@ const AttachmentList: FC<AttachmentListProps> = ({ resource, resourceId }) => {
}
};

if (attachments?.data.length === 0) {
return (
<section>
<SectionHeader title={translate(`shared.attachments.${type}.title`)} />
<Card>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={translate(`shared.attachments.${type}.empty`)}
>
<Upload
accept={type === 'photo' ? 'image/*' : 'application/pdf'}
showUploadList={false}
customRequest={uploadToSupabase}
>
<Button>{translate(`shared.attachments.${type}.add`)}</Button>
</Upload>
</Empty>
</Card>
</section>
);
}

return (
<section>
<SectionHeader title={translate('shared.attachments.title')} />
<SectionHeader title={translate(`shared.attachments.${type}.title`)} />
<List
grid={{ gutter: 8, column: 1 }}
itemLayout="horizontal"
Expand Down Expand Up @@ -141,12 +183,12 @@ const AttachmentList: FC<AttachmentListProps> = ({ resource, resourceId }) => {
)}
/>
<Upload
accept="application/pdf"
accept={type === 'photo' ? 'image/*' : 'application/pdf'}
showUploadList={false}
customRequest={uploadToSupabase}
>
<Button type="link" icon={<PaperClipOutlined />}>
{translate('shared.attachments.add')}
{translate(`shared.attachments.${type}.add`)}
</Button>
</Upload>
</section>
Expand Down
11 changes: 11 additions & 0 deletions src/shared/utils/sanitize-file-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Cleans a file name for S3/Supabase storage:
// - removes accents
// - replaces special characters with _
// - keeps only letters, numbers, . _ -

export function sanitizeFileName(name: string): string {
return name
.normalize('NFD') // removes accents
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-zA-Z0-9._-]/g, '_'); // replaces everything except letters, numbers, . _ -
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
alter table "public"."equipment_attachments" add column "type" text not null default 'document';

alter table "public"."intervention_attachments" add column "type" text not null default 'document';

alter table "public"."equipment_attachments" add constraint "equipment_attachments_type_check" CHECK ((type = ANY (ARRAY['photo'::text, 'document'::text]))) not valid;

alter table "public"."equipment_attachments" validate constraint "equipment_attachments_type_check";

alter table "public"."intervention_attachments" add constraint "intervention_attachments_type_check" CHECK ((type = ANY (ARRAY['photo'::text, 'document'::text]))) not valid;

alter table "public"."intervention_attachments" validate constraint "intervention_attachments_type_check";


1 change: 1 addition & 0 deletions supabase/schemas/equipment_attachments.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ create table public.equipment_attachments (
file_path text not null,
file_name text not null,
file_type text,
type text not null check (type in ('photo', 'document')) default 'document',
description text,
uploaded_at timestamp with time zone not null default now(),
constraint equipment_attachments_pkey primary key (id),
Expand Down
1 change: 1 addition & 0 deletions supabase/schemas/intervention_attachments.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ create table public.intervention_attachments (
file_path text not null,
file_name text not null,
file_type text,
type text not null check (type in ('photo', 'document')) default 'document',
description text,
uploaded_at timestamp with time zone not null default now(),
constraint intervention_attachments_pkey primary key (id),
Expand Down