-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36 from Jethrolopwus/feat/new_payment_links
feat: Implement newPaymentLink
- Loading branch information
Showing
6 changed files
with
337 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -98,4 +98,4 @@ export default function Home() { | |
</footer> | ||
</div> | ||
); | ||
} | ||
} |
181 changes: 181 additions & 0 deletions
181
paystell-frontend/src/components/dashboard/links/newLink/NewLinkForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
"use client"; | ||
import React, { useState } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import * as Form from "@radix-ui/react-form"; | ||
import Image from "next/image"; | ||
import { Button, Input } from "@/components/ui"; | ||
import { Select } from "@/components/ui/select"; | ||
|
||
type FormData = { | ||
title: string; | ||
currency: string; | ||
price: number; | ||
sku?: string; | ||
image: File | null; | ||
}; | ||
|
||
const NewLinks: React.FC = () => { | ||
const { | ||
register, | ||
handleSubmit, | ||
reset, | ||
formState: { errors }, | ||
} = useForm<FormData>(); | ||
const [imagePreview, setImagePreview] = useState<string | null>(null); | ||
|
||
const handleImageUpload = (file: File | null) => { | ||
if (file) { | ||
const reader = new FileReader(); | ||
reader.onloadend = () => { | ||
setImagePreview(reader.result as string); | ||
}; | ||
reader.readAsDataURL(file); | ||
} else { | ||
setImagePreview(null); | ||
} | ||
}; | ||
|
||
const onSubmit = (data: FormData) => { | ||
console.log("Form Submitted:", data); | ||
reset(); | ||
setImagePreview(null); | ||
}; | ||
|
||
return ( | ||
<div className="p-4 flex items-center justify-center"> | ||
<div className="max-w-4xl w-full rounded-lg p-6 overflow-y-auto max-h-[80vh]"> | ||
<Form.Root | ||
className="w-full max-w-lg mx-auto flex flex-col space-y-4" | ||
onSubmit={handleSubmit(onSubmit)} | ||
> | ||
<Form.Field className="grid" name="title"> | ||
<Form.Label className="text-sm font-medium text-gray-800"> | ||
Title | ||
</Form.Label> | ||
<Form.Control asChild> | ||
<Input | ||
type="text" | ||
placeholder="T-shirt" | ||
className="mt-2" | ||
{...register("title", { required: "Title is required" })} | ||
/> | ||
</Form.Control> | ||
{errors.title && ( | ||
<p className="text-red-600 text-sm mt-1"> | ||
{errors.title.message} | ||
</p> | ||
)} | ||
</Form.Field> | ||
|
||
<Form.Field className="grid" name="currency"> | ||
<Form.Label className="text-sm font-medium text-gray-800"> | ||
Currency | ||
</Form.Label> | ||
<Form.Control asChild> | ||
<Select | ||
className="mt-2" | ||
{...register("currency", { required: "Currency is required" })} | ||
> | ||
<option value="" disabled> | ||
Select Currency | ||
</option> | ||
<option value="USD">XLM</option> | ||
<option value="USDC">USDC</option> | ||
|
||
</Select> | ||
</Form.Control> | ||
{errors.currency && ( | ||
<p className="text-red-600 text-sm mt-1"> | ||
{errors.currency.message} | ||
</p> | ||
)} | ||
</Form.Field> | ||
|
||
<Form.Field className="grid" name="price"> | ||
<Form.Label className="text-sm font-medium text-gray-800"> | ||
Price | ||
</Form.Label> | ||
<Form.Control asChild> | ||
<Input | ||
type="number" | ||
placeholder="100 XLM" | ||
className="mt-2" | ||
{...register("price", { | ||
required: "Price is required", | ||
valueAsNumber: true, | ||
validate: (value) => | ||
value > 0 || "Price must be a positive number", | ||
})} | ||
/> | ||
</Form.Control> | ||
{errors.price && ( | ||
<p className="text-red-600 text-sm mt-1"> | ||
{errors.price.message} | ||
</p> | ||
)} | ||
</Form.Field> | ||
|
||
<Form.Field className="grid" name="sku"> | ||
<Form.Label className="text-sm font-medium text-gray-800"> | ||
SKU (Optional) | ||
</Form.Label> | ||
<Form.Control asChild> | ||
<Input | ||
type="text" | ||
placeholder="17639041" | ||
className="mt-2" | ||
{...register("sku")} | ||
/> | ||
</Form.Control> | ||
</Form.Field> | ||
|
||
<Form.Field className="grid" name="image"> | ||
<Form.Label className="text-sm font-medium text-gray-800"> | ||
Upload Image | ||
</Form.Label> | ||
<Form.Control asChild> | ||
<Input | ||
type="file" | ||
accept="image/*" | ||
className="mt-2" | ||
{...register("image", { | ||
required: "Image is required", | ||
validate: (files) => | ||
files instanceof FileList && | ||
files.length > 0 && | ||
files[0] instanceof File | ||
? true | ||
: "Please upload a valid image", | ||
})} | ||
onChange={(e) => | ||
handleImageUpload(e.target.files ? e.target.files[0] : null) | ||
} | ||
/> | ||
</Form.Control> | ||
{errors.image && ( | ||
<p className="text-red-600 text-sm mt-1"> | ||
{errors.image.message} | ||
</p> | ||
)} | ||
{imagePreview && ( | ||
<div className="mt-4 flex justify-center"> | ||
<Image | ||
src={imagePreview} | ||
alt="Preview" | ||
height={100} | ||
width={100} | ||
className="object-contain border rounded-lg" | ||
/> | ||
</div> | ||
)} | ||
</Form.Field> | ||
<Form.Submit asChild> | ||
<Button>+ New Link</Button> | ||
</Form.Submit> | ||
</Form.Root> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default NewLinks; |
20 changes: 20 additions & 0 deletions
20
paystell-frontend/src/components/dashboard/links/newLink/NewLinkModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" | ||
import NewLinkForm from "./NewLinkForm"; | ||
|
||
interface NewLinkModalProps { | ||
isOpen: boolean; | ||
onClose: () => void; | ||
} | ||
|
||
export function NewLinkModal({ isOpen, onClose }: NewLinkModalProps) { | ||
return ( | ||
<Dialog open={isOpen} onOpenChange={onClose}> | ||
<DialogContent> | ||
<DialogHeader> | ||
<DialogTitle>Create New Payment Link</DialogTitle> | ||
</DialogHeader> | ||
<NewLinkForm /> | ||
</DialogContent> | ||
</Dialog> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
"use client" | ||
|
||
import * as React from "react" | ||
import * as DialogPrimitive from "@radix-ui/react-dialog" | ||
import { X } from "lucide-react" | ||
import { cn } from "@/lib/utils" | ||
|
||
const Dialog = DialogPrimitive.Root | ||
|
||
const DialogTrigger = DialogPrimitive.Trigger | ||
|
||
const DialogPortal = DialogPrimitive.Portal | ||
|
||
const DialogClose = DialogPrimitive.Close | ||
|
||
const DialogOverlay = React.forwardRef< | ||
React.ElementRef<typeof DialogPrimitive.Overlay>, | ||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> | ||
>(({ className, ...props }, ref) => ( | ||
<DialogPrimitive.Overlay | ||
ref={ref} | ||
className={cn( | ||
"fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
)) | ||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName | ||
|
||
const DialogContent = React.forwardRef< | ||
React.ElementRef<typeof DialogPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> | ||
>(({ className, children, ...props }, ref) => ( | ||
<DialogPortal> | ||
<DialogOverlay /> | ||
<DialogPrimitive.Content | ||
ref={ref} | ||
className={cn( | ||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", | ||
className | ||
)} | ||
{...props} | ||
> | ||
{children} | ||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> | ||
<X className="h-4 w-4" /> | ||
<span className="sr-only">Close</span> | ||
</DialogPrimitive.Close> | ||
</DialogPrimitive.Content> | ||
</DialogPortal> | ||
)) | ||
DialogContent.displayName = DialogPrimitive.Content.displayName | ||
|
||
const DialogHeader = ({ | ||
className, | ||
...props | ||
}: React.HTMLAttributes<HTMLDivElement>) => ( | ||
<div | ||
className={cn( | ||
"flex flex-col space-y-1.5 text-center sm:text-left", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
) | ||
DialogHeader.displayName = "DialogHeader" | ||
|
||
const DialogFooter = ({ | ||
className, | ||
...props | ||
}: React.HTMLAttributes<HTMLDivElement>) => ( | ||
<div | ||
className={cn( | ||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
) | ||
DialogFooter.displayName = "DialogFooter" | ||
|
||
const DialogTitle = React.forwardRef< | ||
React.ElementRef<typeof DialogPrimitive.Title>, | ||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> | ||
>(({ className, ...props }, ref) => ( | ||
<DialogPrimitive.Title | ||
ref={ref} | ||
className={cn( | ||
"text-lg font-semibold leading-none tracking-tight", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
)) | ||
DialogTitle.displayName = DialogPrimitive.Title.displayName | ||
|
||
const DialogDescription = React.forwardRef< | ||
React.ElementRef<typeof DialogPrimitive.Description>, | ||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> | ||
>(({ className, ...props }, ref) => ( | ||
<DialogPrimitive.Description | ||
ref={ref} | ||
className={cn("text-sm text-muted-foreground", className)} | ||
{...props} | ||
/> | ||
)) | ||
DialogDescription.displayName = DialogPrimitive.Description.displayName | ||
|
||
export { | ||
Dialog, | ||
DialogPortal, | ||
DialogOverlay, | ||
DialogTrigger, | ||
DialogClose, | ||
DialogContent, | ||
DialogHeader, | ||
DialogFooter, | ||
DialogTitle, | ||
DialogDescription, | ||
} |