-
Notifications
You must be signed in to change notification settings - Fork 4
Add option to save on localstorage #14
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,10 @@ import { PlusIcon, TrashIcon } from '@heroicons/react/16/solid'; | |
| import { zodResolver } from '@hookform/resolvers/zod'; | ||
| import { useState } from 'react'; | ||
| import CurrencyInput from 'react-currency-input-field'; | ||
| import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form'; | ||
| import { Controller, SubmitHandler, useFieldArray, useForm } from 'react-hook-form'; | ||
|
|
||
| import { InvoiceFormValues, invoiceSchema } from '@/app/validation'; | ||
| import { useFormStorage } from '@/hooks/useFormStorage'; | ||
| import { Button } from '@/lib/ui/button'; | ||
| import { Divider } from '@/lib/ui/divider'; | ||
| import { Description, ErrorMessage, Field, FieldGroup, Fieldset, Label, Legend } from '@/lib/ui/fieldset'; | ||
|
|
@@ -24,22 +25,35 @@ const generateInvoiceId = () => { | |
| export function InvoiceForm() { | ||
| const [isGenerating, setIsGenerating] = useState(false); | ||
|
|
||
| const defaultValues: InvoiceFormValues = { | ||
| company_name: '', | ||
| company_address: '', | ||
| bill_to: '', | ||
| bill_to_address: '', | ||
| currency: 'USD', | ||
| due_date: '', | ||
| vat_id: '', | ||
| services: [{ description: '', quantity: '1', amount: '' }], | ||
| notes: '', | ||
| }; | ||
|
|
||
| const { | ||
| register, | ||
| control, | ||
| handleSubmit, | ||
| watch, | ||
| getValues, | ||
| reset, | ||
| formState: { errors }, | ||
| } = useForm<InvoiceFormValues>({ | ||
| resolver: zodResolver(invoiceSchema), | ||
| defaultValues: { | ||
| currency: 'USD', | ||
| services: [{ description: '', quantity: '1', amount: '' }], | ||
| }, | ||
| defaultValues, | ||
| }); | ||
|
|
||
| const currency = watch('currency'); | ||
|
|
||
| const { hasSavedData, handleSave, handleLoad, handleClear } = useFormStorage(getValues, reset, defaultValues); | ||
|
|
||
| const { fields, append, remove } = useFieldArray({ | ||
| control, | ||
| name: 'services', | ||
|
|
@@ -172,18 +186,26 @@ export function InvoiceForm() { | |
| </Field> | ||
| <Field className="md:w-64"> | ||
| <Label htmlFor={`services.${index}.amount`}>Amount</Label> | ||
| <CurrencyInput | ||
| key={currency} | ||
| id={`services.${index}.amount`} | ||
| customInput={Input} | ||
| placeholder="e.g., 1500.00" | ||
| allowNegativeValue={false} | ||
| allowDecimals | ||
| intlConfig={{ | ||
| locale: 'en-US', | ||
| currency: currency || 'USD', | ||
| }} | ||
| {...register(`services.${index}.amount`)} | ||
| <Controller | ||
| control={control} | ||
| name={`services.${index}.amount`} | ||
| render={({ field: { onChange, value, name } }) => ( | ||
| <CurrencyInput | ||
| key={currency} | ||
| id={`services.${index}.amount`} | ||
| name={name} | ||
| customInput={Input} | ||
| placeholder="e.g., 1500.00" | ||
| allowNegativeValue={false} | ||
| allowDecimals | ||
| intlConfig={{ | ||
| locale: 'en-US', | ||
| currency: currency || 'USD', | ||
| }} | ||
| value={value} | ||
| onValueChange={(val) => onChange(val ?? '')} | ||
| /> | ||
| )} | ||
| /> | ||
| {errors.services?.[index]?.amount && ( | ||
| <ErrorMessage>{errors.services[index]?.amount?.message}</ErrorMessage> | ||
|
|
@@ -261,13 +283,24 @@ export function InvoiceForm() { | |
| </Field> | ||
| </Fieldset> | ||
|
|
||
| <button | ||
| type="submit" | ||
| className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 mt-10" | ||
| disabled={isGenerating} | ||
| > | ||
| Download PDF | ||
| </button> | ||
| <div className="flex flex-wrap gap-4 mt-10"> | ||
| <Button type="submit" color="indigo" disabled={isGenerating}> | ||
|
Comment on lines
-264
to
+287
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know the styles are a little bit different, but as to me, it makes more sense to use the buttons defined in the UI, so they're all cohesive. |
||
| Download PDF | ||
| </Button> | ||
| <Button type="button" color="emerald" onClick={handleSave}> | ||
| Save | ||
| </Button> | ||
| {hasSavedData && ( | ||
| <> | ||
| <Button type="button" color="zinc" onClick={handleLoad}> | ||
| Load Saved Data | ||
| </Button> | ||
| <Button type="button" color="red" onClick={handleClear}> | ||
| Clear Saved Data | ||
| </Button> | ||
| </> | ||
| )} | ||
| </div> | ||
| </form> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { useEffect, useState } from 'react'; | ||
|
|
||
| import { InvoiceFormValues } from '@/app/validation'; | ||
|
|
||
| const STORAGE_KEY = '@freeinvoice-form-data'; | ||
|
|
||
| export function useFormStorage( | ||
| getValues: () => InvoiceFormValues, | ||
| reset: (values?: Partial<InvoiceFormValues>) => void, | ||
| defaultValues: Partial<InvoiceFormValues> | ||
| ) { | ||
| const [hasSavedData, setHasSavedData] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| try { | ||
| const saved = localStorage.getItem(STORAGE_KEY); | ||
| setHasSavedData(saved !== null); | ||
| } catch { | ||
| // localStorage unavailable | ||
| } | ||
| }, []); | ||
|
|
||
| const handleSave = () => { | ||
| try { | ||
| const data = getValues(); | ||
| localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); | ||
| setHasSavedData(true); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't add any posthog event, but if you want, we can add it. |
||
| } catch { | ||
| // localStorage unavailable | ||
| } | ||
| }; | ||
|
|
||
| const handleLoad = () => { | ||
| try { | ||
| const saved = localStorage.getItem(STORAGE_KEY); | ||
| if (saved) { | ||
| const parsed = JSON.parse(saved) as InvoiceFormValues; | ||
| reset(parsed); | ||
| } | ||
| } catch { | ||
| // corrupted or unavailable | ||
| } | ||
| }; | ||
|
|
||
| const handleClear = () => { | ||
| try { | ||
| localStorage.removeItem(STORAGE_KEY); | ||
| setHasSavedData(false); | ||
| reset(defaultValues); | ||
| } catch { | ||
| // localStorage unavailable | ||
| } | ||
| }; | ||
|
|
||
| return { hasSavedData, handleSave, handleLoad, handleClear }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I needed to change to a controlled input, so it can be reset/loaded with different values, also in order to save only the numbers instead of the full string with currency code