Skip to content
Open
Changes from 1 commit
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
281 changes: 165 additions & 116 deletions apps/frontend/components/builder/forms/projects-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ import { RichTextEditor } from '@/components/ui/rich-text-editor';
import { Project } from '@/components/dashboard/resume-component';
import { Plus, Trash2, Github, Globe } from 'lucide-react';
import { useTranslations } from '@/lib/i18n';
import {
DndContext,
closestCenter,
PointerSensor,
KeyboardSensor,
useSensor,
useSensors,
DragEndEvent,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { DraggableListItem } from '../draggable-list-item';

interface ProjectsFormProps {
data: Project[];
Expand All @@ -17,6 +33,30 @@ interface ProjectsFormProps {
export const ProjectsForm: React.FC<ProjectsFormProps> = ({ data, onChange }) => {
const { t } = useTranslations();

// Configure drag-and-drop sensors
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);

// Handler for drag end event
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;

if (!over || active.id === over.id) return;

const oldIndex = data.findIndex((item) => item.id === active.id);
const newIndex = data.findIndex((item) => item.id === over.id);

if (oldIndex === -1 || newIndex === -1) return;

// Reorder the array using arrayMove from @dnd-kit
const reordered = arrayMove(data, oldIndex, newIndex);
onChange(reordered);
};

const handleAdd = () => {
const newId = Math.max(...data.map((d) => d.id), 0) + 1;
onChange([
Expand Down Expand Up @@ -98,120 +138,8 @@ export const ProjectsForm: React.FC<ProjectsFormProps> = ({ data, onChange }) =>
</Button>
</div>

<div className="space-y-8">
{data.map((item) => (
<div key={item.id} className="p-6 border border-black bg-gray-50 relative group">
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => handleRemove(item.id)}
>
<Trash2 className="w-4 h-4" />
</Button>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 pr-8">
<div className="space-y-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
{t('builder.forms.projects.fields.projectName')}
</Label>
<Input
value={item.name || ''}
onChange={(e) => handleChange(item.id, 'name', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.projectName')}
className="rounded-none border-black bg-white"
/>
</div>
<div className="space-y-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
{t('builder.forms.projects.fields.role')}
</Label>
<Input
value={item.role || ''}
onChange={(e) => handleChange(item.id, 'role', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.role')}
className="rounded-none border-black bg-white"
/>
</div>
<div className="space-y-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
{t('builder.genericItemForm.fields.years')}{' '}
<span className="text-gray-400">({t('common.optional')})</span>
</Label>
<Input
value={item.years || ''}
onChange={(e) => handleChange(item.id, 'years', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.years')}
className="rounded-none border-black bg-white"
/>
</div>
<div className="space-y-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
<Github className="w-3 h-3 inline mr-1" />
GitHub <span className="text-gray-400">({t('common.optional')})</span>
</Label>
<Input
value={item.github || ''}
onChange={(e) => handleChange(item.id, 'github', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.github')}
className="rounded-none border-black bg-white"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
<Globe className="w-3 h-3 inline mr-1" />
{t('builder.forms.projects.fields.website')}{' '}
<span className="text-gray-400">({t('common.optional')})</span>
</Label>
<Input
value={item.website || ''}
onChange={(e) => handleChange(item.id, 'website', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.website')}
className="rounded-none border-black bg-white"
/>
</div>
</div>

<div className="space-y-3">
<div className="flex justify-between items-center">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
{t('builder.genericItemForm.fields.descriptionPoints')}
</Label>
<Button
variant="ghost"
size="sm"
onClick={() => handleAddDescription(item.id)}
className="h-6 text-xs text-blue-700 hover:text-blue-800 hover:bg-blue-50"
>
<Plus className="w-3 h-3 mr-1" /> {t('builder.genericItemForm.actions.addPoint')}
</Button>
</div>
{item.description?.map((desc, idx) => (
<div key={idx} className="flex gap-2">
<div className="flex-1">
<RichTextEditor
value={desc}
onChange={(html) => handleDescriptionChange(item.id, idx, html)}
placeholder={t('builder.forms.projects.placeholders.description')}
minHeight="60px"
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveDescription(item.id, idx)}
className="h-[60px] w-8 text-muted-foreground hover:text-destructive self-end"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
))}
</div>
</div>
))}

{data.length === 0 && (
<div className="text-center py-12 bg-gray-50 border border-dashed border-black">
{data.length === 0 ? (
<div className="text-center py-12 bg-gray-50 border border-dashed border-black">
<p className="font-mono text-sm text-gray-500 mb-4">
{t('builder.genericItemForm.noEntries', { label: t('resume.sections.projects') })}
</p>
Expand All @@ -224,8 +152,129 @@ export const ProjectsForm: React.FC<ProjectsFormProps> = ({ data, onChange }) =>
<Plus className="w-4 h-4 mr-2" /> {t('builder.forms.projects.addFirstProject')}
</Button>
</div>
)}
</div>
) : (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext
items={data.map((item) => item.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-8">
{data.map((item) => (
<DraggableListItem key={item.id} id={item.id}>
<div key={item.id} className="p-6 border border-black bg-gray-50 relative group">
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => handleRemove(item.id)}
>
<Trash2 className="w-4 h-4" />
</Button>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 pr-8">
<div className="space-y-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
{t('builder.forms.projects.fields.projectName')}
</Label>
<Input
value={item.name || ''}
onChange={(e) => handleChange(item.id, 'name', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.projectName')}
className="rounded-none border-black bg-white"
/>
</div>
<div className="space-y-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
{t('builder.forms.projects.fields.role')}
</Label>
<Input
value={item.role || ''}
onChange={(e) => handleChange(item.id, 'role', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.role')}
className="rounded-none border-black bg-white"
/>
</div>
<div className="space-y-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
{t('builder.genericItemForm.fields.years')}{' '}
<span className="text-gray-400">({t('common.optional')})</span>
</Label>
<Input
value={item.years || ''}
onChange={(e) => handleChange(item.id, 'years', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.years')}
className="rounded-none border-black bg-white"
/>
</div>
<div className="space-y-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
<Github className="w-3 h-3 inline mr-1" />
GitHub <span className="text-gray-400">({t('common.optional')})</span>
</Label>
<Input
value={item.github || ''}
onChange={(e) => handleChange(item.id, 'github', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.github')}
className="rounded-none border-black bg-white"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
<Globe className="w-3 h-3 inline mr-1" />
{t('builder.forms.projects.fields.website')}{' '}
<span className="text-gray-400">({t('common.optional')})</span>
</Label>
<Input
value={item.website || ''}
onChange={(e) => handleChange(item.id, 'website', e.target.value)}
placeholder={t('builder.forms.projects.placeholders.website')}
className="rounded-none border-black bg-white"
/>
</div>
</div>

<div className="space-y-3">
<div className="flex justify-between items-center">
<Label className="font-mono text-xs uppercase tracking-wider text-gray-500">
{t('builder.genericItemForm.fields.descriptionPoints')}
</Label>
<Button
variant="ghost"
size="sm"
onClick={() => handleAddDescription(item.id)}
className="h-6 text-xs text-blue-700 hover:text-blue-800 hover:bg-blue-50"
>
<Plus className="w-3 h-3 mr-1" /> {t('builder.genericItemForm.actions.addPoint')}
</Button>
</div>
{item.description?.map((desc, idx) => (
<div key={idx} className="flex gap-2">
<div className="flex-1">
<RichTextEditor
value={desc}
onChange={(html) => handleDescriptionChange(item.id, idx, html)}
placeholder={t('builder.forms.projects.placeholders.description')}
minHeight="60px"
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveDescription(item.id, idx)}
className="h-[60px] w-8 text-muted-foreground hover:text-destructive self-end"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
))}
</div>
</div>
</DraggableListItem>
))}
</div>
</SortableContext>
</DndContext>
)}
</div>
);
};