Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit individual list items. #54

Merged
merged 8 commits into from
Oct 13, 2024
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function App() {
null,
);

const listName = listPath.split('/').pop();
const listName = listPath ? listPath.split('/').pop() : null;

/**
* This custom hook holds info about the current signed in user.
Expand Down
19 changes: 19 additions & 0 deletions src/api/firebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,25 @@ export async function updateItem(listPath, checked, itemData) {
}
}

export async function editItem(
listPath,
id,
{ itemName, itemQuantity, dateNextPurchased },
) {
const listCollectionRef = collection(db, listPath, 'items');
const itemRef = doc(listCollectionRef, id);

try {
await updateDoc(itemRef, {
name: itemName,
quantity: itemQuantity,
dateNextPurchased: dateNextPurchased,
});
} catch (error) {
console.error('There was an error editing the item state: ', error);
}
}

export async function deleteItem(listPath, id) {
const listCollectionRef = collection(db, listPath, 'items');
const itemRef = doc(listCollectionRef, id);
Expand Down
117 changes: 78 additions & 39 deletions src/components/ListItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react';
import { ONE_DAY_IN_MILLISECONDS } from '../utils/dates';
import toast from 'react-hot-toast';
import { Button } from './ui/button';
import { Trash2 } from 'lucide-react';
import { Trash2, Pencil } from 'lucide-react';
import { getIndicatorColor } from '../utils/helpers';
import {
AlertDialog,
Expand All @@ -16,6 +16,16 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from './ui/alert-dialog';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import EditItemForm from './ManageListForms/EditItemForm';

export function ListItem({
listPath,
Expand All @@ -28,6 +38,9 @@ export function ListItem({
dayInterval,
dateCreated,
indicator,
isOpen,
handleOpenModal,
dateNextPurchased,
}) {
const [isAlertOpen, setIsAlertOpen] = useState(false);

Expand Down Expand Up @@ -81,7 +94,7 @@ export function ListItem({
>
<label
htmlFor={`${id}`}
className="capitalize text-sm hover:font-bold sm:text-lg"
className="capitalize text-sm sm:text-base md:text-lg hover:font-bold text-gray-800 dark:text-gray-300"
>
{name}
</label>
Expand All @@ -92,51 +105,77 @@ export function ListItem({
</div>
)}
</div>
<div className="flex items-center gap-1 sm:gap-2">
<div className="flex items-center gap-3">
<div
className={`${getIndicatorColor(indicator)} rounded-[3px] px-2 sm:rounded-[5px] sm:px-3`}
>
<p className="capitalize text-xs sm:text-sm text-black dark:text-gray-800">
{indicator}
</p>
</div>
<AlertDialog open={isAlertOpen} onOpenChange={setIsAlertOpen}>
<AlertDialogTrigger asChild>
<Button
className="bg-transparent hover:bg-transparent"
type="button"
id={id}
onClick={() => setIsAlertOpen(true)}
>
<Trash2 className="w-5 h-5 text-ruby-pink hover:text-opacity-75 dark:text-emerald-500 dark:hover:text-opacity-80 sm:w-6 sm:h-6 md:w-7 md:h-7" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="p-6 sm:p-10">
<AlertDialogHeader>
<AlertDialogTitle className="text-sm text-slate-500 dark:text-slate-400 sm:text-lg">
Are you absolutely sure?
</AlertDialogTitle>
<AlertDialogDescription className="text-black">
This action cannot be undone. Do you really want to delete{' '}
{name}?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
className="bg-white hover:bg-slate-100 px-6 border rounded-lg sm:px-8 sm:rounded-xl"
onClick={() => setIsAlertOpen(false)}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-primary-pink hover:bg-opacity-75 rounded-lg sm:rounded-xl"
onClick={handleDelete}
<div className="flex gap-3 px-1">
<Dialog open={isOpen} onOpenChange={() => handleOpenModal(id)}>
<DialogTrigger asChild>
<Button className="bg-transparent hover:bg-transparent p-0">
<Pencil className="w-5 h-5 text-ruby-pink hover:text-opacity-75 dark:text-emerald-500 dark:hover:text-opacity-80" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit {name}</DialogTitle>
<DialogDescription className="text-md md:text-lg">
Modify the details of the item you&apos;d like to edit.
</DialogDescription>
</DialogHeader>
<EditItemForm
listPath={listPath}
name={name}
id={id}
quantity={quantity}
dateNextPurchased={dateNextPurchased}
handleOpenModal={handleOpenModal}
/>
<DialogFooter className="sm:justify-start"></DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog open={isAlertOpen} onOpenChange={setIsAlertOpen}>
<AlertDialogTrigger asChild>
<Button
className="bg-transparent hover:bg-transparent p-0"
type="button"
id={id}
onClick={() => setIsAlertOpen(true)}
>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Trash2 className="w-5 h-5 text-ruby-pink hover:text-opacity-75 dark:text-emerald-500 dark:hover:text-opacity-80" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="p-6 sm:p-10">
<AlertDialogHeader>
<AlertDialogTitle className="text-sm text-slate-800 dark:text-slate-400 sm:text-lg">
Are you absolutely sure?
</AlertDialogTitle>
<AlertDialogDescription className="text-slate-700">
This action cannot be undone. Do you really want to delete{' '}
{name}?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
className="bg-white text-slate-700 hover:bg-slate-100 px-6 border rounded-lg sm:px-8 sm:rounded-xl"
onClick={() => setIsAlertOpen(false)}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-primary-pink text-white hover:bg-opacity-75 px-6 border rounded-lg sm:px-8 sm:rounded-xl"
onClick={handleDelete}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</li>
);
Expand Down
179 changes: 179 additions & 0 deletions src/components/ManageListForms/EditItemForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { useEffect, useState } from 'react';
import { editItem } from '../../api/firebase';
import toast from 'react-hot-toast';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { getFutureDate } from '../../utils/dates';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';

export default function EditItemForm({
listPath,
id,
name,
quantity,
dateNextPurchased,
handleOpenModal,
}) {
const [formData, setFormData] = useState({
itemName: name || '',
itemQuantity: quantity || 1,
daysUntilNextPurchase: dateNextPurchased || '',
});

useEffect(() => {
setFormData({
itemName: name,
itemQuantity: quantity,
daysUntilNextPurchase: dateNextPurchased,
});
}, []);

const handleSubmit = async (event) => {
event.preventDefault();

const formattedItemName = formData.itemName
.toLowerCase()
.replace(/^\s\s*/, '')
.replace(/\s\s*$/, '')
.replace(/[^a-zA-Z ]/g, '');

const formQuantity = parseInt(formData.itemQuantity, 10);
const newDateNextPurchased = formData.daysUntilNextPurchase;

if (formattedItemName.length === 0) {
toast.error(`No numbers or special characters.`);
resetItemName();
return;
}
// check if any of the values have been edited
const hasChanged =
formattedItemName !== name ||
formQuantity !== quantity ||
newDateNextPurchased !== dateNextPurchased;

// if no changes were made exit early
if (!hasChanged) {
toast.error('No changes were made.');
handleOpenModal();
return;
}

let updatedData = {
itemName: formattedItemName,
itemQuantity: formQuantity,
};
// check if dateNextPurchased have been edited to use getFutureDate if not use the same date
if (newDateNextPurchased !== dateNextPurchased) {
const futureDate = parseInt(newDateNextPurchased);
updatedData.dateNextPurchased = getFutureDate(futureDate);
} else {
updatedData.dateNextPurchased = dateNextPurchased;
}

try {
handleOpenModal();
await editItem(listPath, id, updatedData);
toast.success(`${formattedItemName} has been updated!`);
} catch (error) {
toast.error(`Failed to edit ${formattedItemName}`);
}
};

const handleChange = (event) => {
let { name, value } = event.target;
setFormData((prevFormData) => ({ ...prevFormData, [name]: value }));
};

const handleRadioChange = (value) => {
setFormData((prevFormData) => ({
...prevFormData,
daysUntilNextPurchase: value,
}));
};

return (
<form
className="flex flex-col items-center gap-8 max-w-[400px] text-black"
onSubmit={(event) => handleSubmit(event)}
>
<div className="flex flex-col gap-2 w-full">
<label htmlFor="itemName" className="text-md font-medium">
Item name{' '}
</label>
<Input
type="text"
name="itemName"
id="itemName"
placeholder="Enter item name"
required
value={formData.itemName}
onChange={handleChange}
/>
</div>
<div className="flex flex-col gap-2 w-full">
<label htmlFor="daysUntilNextPurchase" className="text-md font-medium">
How soon would you like to buy this again?
</label>
<RadioGroup
onValueChange={handleRadioChange}
className="flex my-2 items-center justify-center gap-4"
id="daysUntilNextPurchase"
>
<div className="flex flex-col items-center justify-center rounded-xl border border-light-pink gap-4 w-24 h-24 sm:w-28 sm:h-28 shadow-bottom-right transition-transform duration-200 ease-in-out transform active:scale-95">
<RadioGroupItem
value="7"
id="soon"
name="timeFrame"
className="border border-soon text-soon"
/>
<label htmlFor="soon" className="font-semibold text-sm">
Soon
</label>
</div>
<div className="flex flex-col items-center justify-center rounded-xl border border-light-pink gap-4 w-24 h-24 sm:w-28 sm:h-28 shadow-bottom-right transition-transform duration-200 ease-in-out transform active:scale-95">
<RadioGroupItem
value="14"
id="kind-of-soon"
name="timeFrame"
className="border border-kind-of-soon text-kind-of-soon"
/>
<label htmlFor="kind-of-soon" className="font-semibold text-sm">
Kind of soon
</label>
</div>
<div className="flex flex-col items-center justify-center rounded-xl border border-light-pink gap-4 w-24 h-24 sm:w-28 sm:h-28 shadow-bottom-right transition-transform duration-200 ease-in-out transform active:scale-95">
<RadioGroupItem value="30" id="not-of-soon" name="timeFrame" />
<label htmlFor="not of soon" className="font-semibold text-sm">
Not soon
</label>
</div>
</RadioGroup>
</div>
<div className="flex flex-col items-start gap-2 w-full">
<label htmlFor="quantity" className="text-md font-medium">
Quantity
</label>
<div className="flex items-start">
<Input
type="number"
name="itemQuantity"
id="quantity"
required
value={formData.itemQuantity}
onChange={handleChange}
min={1}
max={100}
/>
</div>
</div>
<div className="flex w-full">
<Button
type="submit"
className="bg-primary-pink text-white rounded-xl w-full hover:bg-primary-pink hover:bg-opacity-75 text-sm"
Hudamabkhoot marked this conversation as resolved.
Show resolved Hide resolved
>
Apply Changes
</Button>
</div>
</form>
);
}
2 changes: 1 addition & 1 deletion src/components/ui/dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 rounded-xl',
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
Expand Down
Loading