Merge branch 'front-end' of https://github.com/Sosokker/B2D-Ventures into front-end

This commit is contained in:
Naytitorn Chaovirachot 2024-11-06 16:56:08 +07:00
commit 54db6905da
6 changed files with 172 additions and 237 deletions

View File

@ -16,13 +16,14 @@ interface ItemProps {
alt: string;
width: number;
height: number;
className?: string;
}
const ImageModal = ({ src, alt, width, height }: ItemProps) => {
const ImageModal = ({ src, alt, width, height, className }: ItemProps) => {
return (
<Dialog>
<DialogTrigger asChild>
<Image src={src} alt={alt} width={width} height={height} className="rounded-lg basis-0" />
<Image src={src} alt={alt} width={width} height={height} className={className} />
</DialogTrigger>
<DialogContent>
<DialogHeader>
@ -36,6 +37,6 @@ const ImageModal = ({ src, alt, width, height }: ItemProps) => {
);
};
export function DisplayFullImage({ src, alt, width, height }: ItemProps) {
return <ImageModal src={src} alt={alt} width={width} height={height} />;
export function DisplayFullImage({ src, alt, width, height, className }: ItemProps) {
return <ImageModal src={src} alt={alt} width={width} height={height} className={className} />;
}

View File

@ -12,16 +12,25 @@ import { Separator } from "@/components/ui/separator";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import FollowShareButtons from "./followShareButton";
import { DisplayFullImage } from "./displayImage";
import { getProjectData } from "@/lib/data/projectQuery";
import { getDealList } from "@/app/api/dealApi";
import { sumByKey, toPercentage } from "@/lib/utils";
import { redirect } from "next/navigation";
const PHOTO_MATERIAL_ID = 2;
export default async function ProjectDealPage({ params }: { params: { id: number } }) {
const supabase = createSupabaseClient();
const { data: projectData, error: projectDataError } = await getProjectData(supabase, params.id);
const { data: projectMaterial, error: projectMaterialError } = await supabase
.from("project_material")
.select("material_url")
.eq("project_id", params.id)
.eq("material_type_id", PHOTO_MATERIAL_ID);
// console.log(projectMaterial);
if (projectMaterialError) {
console.error("Error while fetching project material" + projectMaterialError);
}
if (!projectData) {
redirect("/deals");
}
@ -44,96 +53,107 @@ export default async function ProjectDealPage({ params }: { params: { id: number
const timeDiff = Math.max(new Date(projectData.investment_deadline).getTime() - new Date().getTime(), 0);
const hourLeft = Math.floor(timeDiff / (1000 * 60 * 60));
const carouselData = Array(5).fill({
src: projectData.card_image_url || "/boiler1.jpg",
alt: `${projectData.project_name} Image`,
});
const carouselData =
projectMaterial && projectMaterial.length > 0
? projectMaterial.flatMap((item) =>
(item.material_url || ["/boiler1.jpg"]).map((url: string) => ({
src: url,
alt: "Image",
}))
)
: [{ src: "/boiler1.jpg", alt: "Default Boiler Image" }];
// console.log(carouselData);
return (
<div className="container max-w-screen-xl my-5">
<div className="flex flex-col gap-y-10">
<div id="content">
{/* Name, star and share button packed */}
<div id="header" className="flex flex-col">
<div className="flex justify-between">
<span className="flex">
<Image src="/logo.svg" alt="logo" width={50} height={50} className="sm:scale-75" />
<h1 className="mt-3 font-bold text-lg md:text-3xl">{projectData?.project_name}</h1>
</span>
<FollowShareButtons />
</div>
{/* end of pack */}
<p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p>
<div className="flex flex-wrap mt-3">
{projectData?.tags.map((tag, index) => (
<span key={index} className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1">
{tag.tag_name}
</span>
))}
</div>
{/* Name, star and share button packed */}
<div id="header" className="flex flex-col">
<div className="flex justify-between">
<span className="flex">
<Image src="/logo.svg" alt="logo" width={50} height={50} className="sm:scale-75" />
<h1 className="mt-3 font-bold text-lg md:text-3xl">{projectData?.project_name}</h1>
</span>
<FollowShareButtons />
</div>
{/* end of pack */}
<p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p>
<div className="flex flex-wrap mt-3">
{projectData?.tags.map((tag, index) => (
<span key={index} className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1">
{tag.tag_name}
</span>
))}
</div>
</div>
<div id="sub-content" className="flex flex-row mt-5">
{/* image carousel */}
<div id="image-carousel" className="shrink-0 w-[700px] flex flex-col">
{/* first carousel */}
<Carousel className="w-full h-[400px] ml-1 overflow-hidden">
<CarouselContent className="flex h-full">
{carouselData.map((item, index) => (
<CarouselItem key={index}>
<Image src={item.src} alt={item.alt} width={700} height={400} className="rounded-lg object-cover" />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 transform -translate-y-1/2 z-10 text-white bg-black opacity-50 hover:opacity-100" />
<CarouselNext className="absolute right-2 top-1/2 transform -translate-y-1/2 z-10 text-white bg-black opacity-50 hover:opacity-100" />
</Carousel>
{/* second carousel */}
<Carousel className="w-full ml-1 h-[100px] mt-5 overflow-hidden">
<CarouselContent className="flex space-x-1 h-[100px]">
{carouselData.map((item, index) => (
<CarouselItem key={index} className="flex">
<DisplayFullImage
src={item.src}
alt={item.alt}
width={200}
height={100}
className="rounded-lg object-cover h-[100px] w-[200px]"
/>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
<div id="sub-content" className="flex flex-row mt-5">
{/* image carousel */}
<div id="image-corousel" className="shrink-0 w-[700px] flex flex-col">
<Carousel className="w-full h-full ml-1">
<CarouselContent className="flex h-full">
{carouselData.map((item, index) => (
<CarouselItem key={index}>
<Image src={item.src} alt={item.alt} width={700} height={400} className="rounded-lg" />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<Carousel className="w-full ml-1 h-[100px] mt-5">
<CarouselContent className="flex space-x-1">
{carouselData.map((item, index) => (
<CarouselItem key={index} className="flex">
<CarouselItem key={index} className="flex">
<DisplayFullImage src="/path/to/image.jpg" alt="Image description" width={300} height={200} />
</CarouselItem>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
<div id="stats" className="flex flex-col w-full mt-4 pl-12">
<div className="pl-5">
<span>
<h1 className="font-semibold text-xl md:text-4xl mt-8">${totalDealAmount}</h1>
<p className="text-sm md:text-lg">
{toPercentage(totalDealAmount, projectData?.target_investment)}% raised of $
{projectData?.target_investment} max goal
</p>
<Progress
value={toPercentage(totalDealAmount, projectData?.target_investment)}
className="w-[60%] h-3 mt-3"
/>
</span>
<span>
<h1 className="font-semibold text-4xl md:mt-8">
<p className="text-xl md:text-4xl">{dealList.length}</p>
</h1>
<p className="text-sm md:text-lg">Investors</p>
</span>
<Separator decorative className="mt-3 w-3/4 ml-5" />
<span>
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5"></h1>
{projectData?.investment_deadline ? (
<>
<p className="text-xl md:text-4xl">{Math.floor(hourLeft)} hours</p>
<p>Left to invest</p>
</>
) : (
<p className="text-xl md:text-4xl">No deadline</p>
)}
</span>
<Button className="mt-5 w-3/4 h-12">
<Link href={`/invest/${params.id}`}>Invest in {projectData?.project_name}</Link>
</Button>
</div>
<div id="stats" className="flex flex-col w-full mt-4 pl-12">
<div className="pl-5">
<span>
<h1 className="font-semibold text-xl md:text-4xl mt-8">${totalDealAmount}</h1>
<p className="text-sm md:text-lg">
{toPercentage(totalDealAmount, projectData?.target_investment)}% raised of $
{projectData?.target_investment} max goal
</p>
<Progress
value={toPercentage(totalDealAmount, projectData?.target_investment)}
className="w-[60%] h-3 mt-3"
/>
</span>
<span>
<h1 className="font-semibold text-4xl md:mt-8">
<p className="text-xl md:text-4xl">{dealList.length}</p>
</h1>
<p className="text-sm md:text-lg">Investors</p>
</span>
<Separator decorative className="mt-3 w-3/4 ml-5" />
<span>
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5"></h1>
{projectData?.investment_deadline ? (
<>
<p className="text-xl md:text-4xl">{Math.floor(hourLeft)} hours</p>
<p>Left to invest</p>
</>
) : (
<p className="text-xl md:text-4xl">No deadline</p>
)}
</span>
<Button className="mt-5 w-3/4 h-12">
<Link href={`/invest/${params.id}`}>Invest in {projectData?.project_name}</Link>
</Button>
</div>
</div>
</div>
@ -142,9 +162,7 @@ export default async function ProjectDealPage({ params }: { params: { id: number
<div className="flex w-fit">
<Tabs.Root defaultValue="pitch">
<Tabs.List className="list-none flex gap-10 text-lg md:text-xl">
<Tabs.Trigger value="pitch" className="text-yellow-300">
Pitch
</Tabs.Trigger>
<Tabs.Trigger value="pitch">Pitch</Tabs.Trigger>
<Tabs.Trigger value="general">General Data</Tabs.Trigger>
<Tabs.Trigger value="update">Updates</Tabs.Trigger>
</Tabs.List>

View File

@ -123,7 +123,9 @@ export default async function Home() {
<CardContent className="flex gap-2">
<Link href="https://github.com/Sosokker/B2D-Ventures" passHref>
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
<div className="dark:bg-white rounded-full">
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
</div>
Github
</Button>
</Link>

View File

@ -13,7 +13,7 @@ import {
checkForInvest,
getLatestInvestment,
getTotalInvestment,
} from "./hook";
} from "./query";
import CountUpComponent from "@/components/countUp";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";

View File

@ -2,14 +2,7 @@ import { useEffect, useState } from "react";
import { SubmitHandler, useForm, ControllerRenderProps } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { MultipleOptionSelector } from "@/components/multipleSelector";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { projectFormSchema } from "@/types/schemas/application.schema";
import { z } from "zod";
@ -17,19 +10,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Label } from "@/components/ui/label";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { Textarea } from "./ui/textarea";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { ChevronsUpDown, Check, X } from "lucide-react";
@ -39,30 +21,21 @@ type FieldType = ControllerRenderProps<any, "projectPhotos">;
interface ProjectFormProps {
onSubmit: SubmitHandler<projectSchema>;
}
const ProjectForm = ({
onSubmit,
}: ProjectFormProps & { onSubmit: SubmitHandler<projectSchema> }) => {
const ProjectForm = ({ onSubmit }: ProjectFormProps & { onSubmit: SubmitHandler<projectSchema> }) => {
const form = useForm<projectSchema>({
resolver: zodResolver(projectFormSchema),
defaultValues: {},
});
let supabase = createSupabaseClient();
const [projectType, setProjectType] = useState<
{ id: number; name: string }[]
>([]);
const [projectType, setProjectType] = useState<{ id: number; name: string }[]>([]);
const [projectPitch, setProjectPitch] = useState("text");
const [selectedImages, setSelectedImages] = useState<File[]>([]);
const [projectPitchFile, setProjectPitchFile] = useState("");
const [tag, setTag] = useState<{ id: number; value: string }[]>([]);
const [open, setOpen] = useState(false);
const [selectedTag, setSelectedTag] = useState<
{ id: number; value: string }[]
>([]);
const [selectedTag, setSelectedTag] = useState<{ id: number; value: string }[]>([]);
const handleFileChange = (
event: React.ChangeEvent<HTMLInputElement>,
field: FieldType
) => {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, field: FieldType) => {
if (event.target.files) {
const filesArray = Array.from(event.target.files);
console.log("first file", filesArray);
@ -86,9 +59,7 @@ const ProjectForm = ({
};
const fetchProjectType = async () => {
let { data: ProjectType, error } = await supabase
.from("project_type")
.select("id, value");
let { data: ProjectType, error } = await supabase.from("project_type").select("id, value");
if (error) {
console.error(error);
@ -125,10 +96,7 @@ const ProjectForm = ({
}, []);
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit as SubmitHandler<projectSchema>)}
className="space-y-8"
>
<form onSubmit={form.handleSubmit(onSubmit as SubmitHandler<projectSchema>)} className="space-y-8">
<div className="ml-96 space-y-10">
{/* project name */}
<FormField
@ -137,17 +105,10 @@ const ProjectForm = ({
render={({ field }: { field: any }) => (
<FormItem>
<div className="space-y-5">
<FormLabel className="font-bold text-lg">
Project name
</FormLabel>
<FormLabel className="font-bold text-lg">Project name</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
type="text"
id="projectName"
className="w-96"
{...field}
/>
<Input type="text" id="projectName" className="w-96" {...field} />
</div>
</FormControl>
</div>
@ -169,9 +130,7 @@ const ProjectForm = ({
handleFunction={(selectedValues: any) => {
field.onChange(selectedValues.id);
}}
description={
<>Please specify the primary purpose of the funds</>
}
description={<>Please specify the primary purpose of the funds</>}
placeholder="Select a Project type"
selectLabel="Project type"
/>
@ -189,18 +148,11 @@ const ProjectForm = ({
<FormItem>
<FormControl>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">
Short description
</FormLabel>
<FormLabel className="font-bold text-lg">Short description</FormLabel>
<div className="flex space-x-5">
<Textarea
id="shortDescription"
className="w-96"
{...field}
/>
<Textarea id="shortDescription" className="w-96" {...field} />
<span className="text-[12px] text-neutral-500 self-center">
Could you provide a brief description of your project{" "}
<br /> in one or two sentences?
Could you provide a brief description of your project <br /> in one or two sentences?
</span>
</div>
</div>
@ -225,9 +177,7 @@ const ProjectForm = ({
<div className="flex space-x-2 w-96">
<Button
type="button"
variant={
projectPitch === "text" ? "default" : "outline"
}
variant={projectPitch === "text" ? "default" : "outline"}
onClick={() => setProjectPitch("text")}
className="w-32 h-12 text-base"
>
@ -235,9 +185,7 @@ const ProjectForm = ({
</Button>
<Button
type="button"
variant={
projectPitch === "file" ? "default" : "outline"
}
variant={projectPitch === "file" ? "default" : "outline"}
onClick={() => setProjectPitch("file")}
className="w-32 h-12 text-base"
>
@ -247,11 +195,7 @@ const ProjectForm = ({
<div className="flex space-x-5">
<Input
type={projectPitch === "file" ? "file" : "text"}
placeholder={
projectPitch === "file"
? "Upload your Markdown file"
: "https:// "
}
placeholder={projectPitch === "file" ? "Upload your Markdown file" : "https:// "}
accept={projectPitch === "file" ? ".md" : undefined}
onChange={(e) => {
const value = e.target;
@ -266,11 +210,9 @@ const ProjectForm = ({
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload a file or paste a link to your pitch,
which should <br />
cover key aspects of your project: what it will do,
what investors <br /> can expect to gain, and any
highlights that make it stand out.
Please upload a file or paste a link to your pitch, which should <br />
cover key aspects of your project: what it will do, what investors <br /> can expect to gain,
and any highlights that make it stand out.
</span>
</div>
{projectPitchFile && (
@ -302,23 +244,22 @@ const ProjectForm = ({
<FormItem>
<FormControl>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg mt-10">
Project logo
</FormLabel>
<Input
type="file"
id="projectLogo"
className="w-96"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
field.onChange(file || "");
}}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload the logo picture that best represents your
project.
</span>
<FormLabel className="font-bold text-lg mt-10">Project logo</FormLabel>
<div className="flex space-x-5">
<Input
type="file"
id="projectLogo"
className="w-96"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
field.onChange(file || "");
}}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload the logo picture that best represents your project.
</span>
</div>
</div>
</FormControl>
<FormMessage />
@ -334,9 +275,7 @@ const ProjectForm = ({
<FormItem>
<FormControl>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg mt-10">
Project photos
</FormLabel>
<FormLabel className="font-bold text-lg mt-10">Project photos</FormLabel>
<div className="flex space-x-5">
<Input
type="file"
@ -349,16 +288,15 @@ const ProjectForm = ({
}}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload the logo picture that best represents your
project.
Please upload the photo that best represents your project.
<p className="text-red-500">
*** It is recommended that the photo be horizontal for better presentation.
</p>
</span>
</div>
<div className="mt-5 space-y-2 w-96">
{selectedImages.map((image, index) => (
<div
key={index}
className="flex justify-between items-center border p-2 rounded"
>
<div key={index} className="flex justify-between items-center border p-2 rounded">
<span>{image.name}</span>
<Button
variant="outline"
@ -385,9 +323,7 @@ const ProjectForm = ({
render={({ field }: { field: any }) => (
<FormItem>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">
Minimum investment
</FormLabel>
<FormLabel className="font-bold text-lg">Minimum investment</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
@ -419,9 +355,7 @@ const ProjectForm = ({
render={({ field }: { field: any }) => (
<FormItem>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">
Target investment
</FormLabel>
<FormLabel className="font-bold text-lg">Target investment</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
@ -437,8 +371,8 @@ const ProjectForm = ({
value={field.value}
/>
<span className="text-[12px] text-neutral-500 self-center">
We encourage you to set a specific target investment{" "}
<br /> amount that reflects your funding goals.
We encourage you to set a specific target investment <br /> amount that reflects your funding
goals.
</span>
</div>
</FormControl>
@ -457,16 +391,10 @@ const ProjectForm = ({
<FormLabel className="font-bold text-lg">Deadline</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
type="datetime-local"
id="deadline"
className="w-96"
{...field}
/>
<Input type="datetime-local" id="deadline" className="w-96" {...field} />
<span className="text-[12px] text-neutral-500 self-center">
What is the deadline for your fundraising project?
Setting <br /> a clear timeline can help motivate
potential investors.
What is the deadline for your fundraising project? Setting <br /> a clear timeline can help
motivate potential investors.
</span>
</div>
</FormControl>
@ -493,9 +421,7 @@ const ProjectForm = ({
aria-expanded={open}
className="w-96 justify-between overflow-hidden text-ellipsis whitespace-nowrap"
>
{selectedTag.length > 0
? selectedTag.map((t) => t.value).join(", ")
: "Select tags..."}
{selectedTag.length > 0 ? selectedTag.map((t) => t.value).join(", ") : "Select tags..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
@ -511,15 +437,11 @@ const ProjectForm = ({
value={tag.value}
onSelect={() => {
setSelectedTag((prev) => {
const exists = prev.find(
(t) => t.id === tag.id
);
const exists = prev.find((t) => t.id === tag.id);
const updatedTags = exists
? prev.filter((t) => t.id !== tag.id)
: [...prev, tag];
field.onChange(
updatedTags.map((t) => t.id)
);
field.onChange(updatedTags.map((t) => t.id));
return updatedTags;
});
setOpen(false);
@ -528,9 +450,7 @@ const ProjectForm = ({
<Check
className={cn(
"h-4",
selectedTag.some((t) => t.id === tag.id)
? "opacity-100"
: "opacity-0"
selectedTag.some((t) => t.id === tag.id) ? "opacity-100" : "opacity-0"
)}
/>
{tag.value}
@ -542,8 +462,7 @@ const ProjectForm = ({
</PopoverContent>
</Popover>
<span className="text-[12px] text-neutral-500 self-center">
Add 1 to 5 tags that describe your project. Tags help{" "}
<br />
Add 1 to 5 tags that describe your project. Tags help <br />
investors understand your focus.
</span>
</div>
@ -561,9 +480,7 @@ const ProjectForm = ({
<button
onClick={() => {
setSelectedTag((prev) => {
const updatedTags = prev.filter(
(t) => t.id !== tag.id
);
const updatedTags = prev.filter((t) => t.id !== tag.id);
field.onChange(updatedTags.map((t) => t.id));
return updatedTags;
});
@ -579,10 +496,7 @@ const ProjectForm = ({
/>
</div>
<center>
<Button
className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5 "
type="submit"
>
<Button className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5 " type="submit">
Submit application
</Button>
</center>