diff --git a/app/[short_id]/route.ts b/app/[short_id]/route.ts index d1b9eec..d017d5b 100644 --- a/app/[short_id]/route.ts +++ b/app/[short_id]/route.ts @@ -5,19 +5,32 @@ import { publishUserAgent, } from "@/lib/services/redisPublicGenerate"; import { NextRequest } from "next/server"; +import PrismaClientManager from "@/lib/services/pgConnect"; export async function GET(req: NextRequest) { const path = req.nextUrl.pathname; const shortCode = path.replace("/", ""); if (!checkIfShortCodePublic(shortCode)) { - return RESPONSE( - { - error: "Invalid input", - moreinfo: "We are currently not supporting private short codes", - }, - HTTP_STATUS.BAD_REQUEST - ); + // click analytics yet to be added + const prisma = PrismaClientManager.getInstance().getPrismaClient(); + const link:any = await prisma.links.findFirst({ + where:{ + short_code:shortCode + } + }) + + if(!link){ + return RESPONSE( + { + error: "Invalid input", + moreinfo: "Short link generated is invalid or expired", + }, + HTTP_STATUS.BAD_REQUEST + ); + } + + return Response.redirect(link?.long_url,301) } const long_url = await getLongUrl(shortCode); diff --git a/app/app/(dashboard)/links/[id]/page.tsx b/app/app/(dashboard)/links/[id]/page.tsx index fccb6e2..e8c7438 100644 --- a/app/app/(dashboard)/links/[id]/page.tsx +++ b/app/app/(dashboard)/links/[id]/page.tsx @@ -17,6 +17,7 @@ import { LinkShareDialog } from "@/components/DialogComponents/LinkShareDialog"; import { EditLinkDialog } from "@/components/DialogComponents/EditLinkDialog"; import { copyToClipboard } from "@/lib/utils"; import { LinkType } from "@/interfaces/types"; +import { useState } from "react"; const months = [ "January", "February", "March", "April", "May", "June", @@ -63,6 +64,20 @@ export default function Page(params : any) { } }; + const fetchLink = { + id: 1, + user_id: 42, + short_code: 'e7b9f3', + long_url: 'https://example.com/e7b9f3a7-0a15-4b7d-8d62-0d5f1a52e73e', + created_at: new Date('2023-05-28T12:34:56Z'), + title: 'Sample Title' + } + + const REDIRECT_URL:string = process.env.REDIRECT_URL || "https://eurl.vshetty.dev"; + const [title,setTitle] = useState(fetchLink.title); + const [shortCode,setShortcode] = useState(fetchLink.short_code) + const shortLink:string = `${REDIRECT_URL}/${shortCode}` + return (
@@ -81,20 +96,20 @@ export default function Page(params : any) {

- {link.title} + {title}

- - + - +
@@ -119,21 +134,21 @@ export default function Page(params : any) {
-

{months[link.dateCreated.getMonth()]} {link.dateCreated.getDate()} {link.dateCreated.getFullYear()}

+

{months[fetchLink.created_at.getMonth()]} {fetchLink.created_at.getDate()} {fetchLink.created_at.getFullYear()}

- - + - + diff --git a/app/app/(dashboard)/links/create/page.tsx b/app/app/(dashboard)/links/create/page.tsx index a6c8ff0..2c0159c 100644 --- a/app/app/(dashboard)/links/create/page.tsx +++ b/app/app/(dashboard)/links/create/page.tsx @@ -116,7 +116,7 @@ export default function CreatePage() {
setShortLink(e.target.value)} className="mt-2 md:mt-0" diff --git a/app/app/(dashboard)/links/page.tsx b/app/app/(dashboard)/links/page.tsx index 4175e62..5010874 100644 --- a/app/app/(dashboard)/links/page.tsx +++ b/app/app/(dashboard)/links/page.tsx @@ -1,21 +1,21 @@ "use client" import { DatePickerWithRange } from "@/components/DialogComponents/DatePickerWithRange"; -import React, { useEffect, useMemo, useState } from 'react' +import React, { useEffect, useState } from 'react' import { FilterDialog } from "@/components/DialogComponents/FilterDialog"; import { LinkCard } from "@/components/CardComponents/LinkCard"; -import { LinkType } from "@/interfaces/types"; + import { getLinks } from "@/lib/actions/getLinksAction"; import { DateRange } from "react-day-picker"; -import { addDays } from "date-fns"; -import { Divide } from "lucide-react"; import Loading from "./loading"; +import { linkType } from "@/interfaces/types"; + export default function Page() { - const [filteredLinks,setFilteredLinks] = useState([]) - const [loading,setLoading] = useState(true) + const [filteredLinks,setFilteredLinks] = useState([]) + const [loading,setLoading] = useState(true) const current_date = new Date() const [date, setDate] = React.useState({ to: new Date(current_date.getFullYear(), current_date.getMonth(), current_date.getDate()+1), @@ -24,11 +24,11 @@ export default function Page() { useEffect(()=>{ setLoading(true) - getLinks().then((res:any)=>{ - const linkList = res.links; - const from:any = date?.from; - const to:any = date?.to; - const filterLinks = linkList?.filter((link:any)=>{ + getLinks().then((res)=>{ + const linkList:linkType[] | undefined = res.links; + const from: Date | undefined = date?.from; + const to: Date | undefined = date?.to; + const filterLinks: linkType[] | undefined = linkList?.filter((link)=>{ return (!from || link.created_at >=from) && (!to || link.created_at <= to) }) setFilteredLinks(filterLinks) @@ -49,7 +49,7 @@ export default function Page() { - {loading?:
{filteredLinks?.map((link:any) => ( + {loading?:
{filteredLinks?.map((link) => ( ))}
} diff --git a/app/app/(dashboard)/qrcodes/page.tsx b/app/app/(dashboard)/qrcodes/page.tsx index 603e63c..5e827ca 100644 --- a/app/app/(dashboard)/qrcodes/page.tsx +++ b/app/app/(dashboard)/qrcodes/page.tsx @@ -1,104 +1,21 @@ "use client" import { QRCodeCardComponent } from "@/components/CardComponents/QRCodeCardComponent"; -import { QRCodeType } from "@/interfaces/types"; +import { QRCodeType, linkType } from "@/interfaces/types"; import { getLinks } from "@/lib/actions/getLinksAction"; import { Label } from "@radix-ui/react-label"; import { useEffect, useState } from "react"; import Loading from "./loading"; - -const dummyQRCodeData: QRCodeType[] = [ - { - id: 1, - title: "Exclusive Product Launch Event", - shortLink: "qr-shortlink-1", - longLink: "https://example.com/longlink-1", - scans: 128, - dateCreated: new Date("2024-04-30T08:00:00") - }, - { - id: 2, - title: "Limited Time Offer: 50% Discount", - shortLink: "qr-shortlink-2", - longLink: "https://example.com/longlink-2", - scans: 75, - dateCreated: new Date("2024-03-29T12:30:00") - }, - { - id: 3, - title: "Free Webinar on Digital Marketing Trends", - shortLink: "qr-shortlink-3", - longLink: "https://example.com/longlink-3", - scans: 250, - dateCreated: new Date("2024-02-28T15:45:00") - }, - { - id: 4, - title: "New Feature Showcase: Mobile App", - shortLink: "qr-shortlink-4", - longLink: "https://example.com/longlink-4", - scans: 390, - dateCreated: new Date("2024-01-27T11:20:00") - }, - { - id: 5, - title: "Exclusive Customer Rewards Program", - shortLink: "qr-shortlink-5", - longLink: "https://example.com/longlink-5", - scans: 563, - dateCreated: new Date("2023-12-26T09:10:00") - }, - { - id: 6, - title: "Launch of New Product Line", - shortLink: "qr-shortlink-6", - longLink: "https://example.com/longlink-6", - scans: 197, - dateCreated: new Date("2023-11-25T14:00:00") - }, - { - id: 7, - title: "Customer Feedback Survey", - shortLink: "qr-shortlink-7", - longLink: "https://example.com/longlink-7", - scans: 821, - dateCreated: new Date("2023-10-24T10:05:00") - }, - { - id: 8, - title: "Special Holiday Season Offer", - shortLink: "qr-shortlink-8", - longLink: "https://example.com/longlink-8", - scans: 632, - dateCreated: new Date("2023-09-23T13:40:00") - }, - { - id: 9, - title: "Product Launch Giveaway", - shortLink: "qr-shortlink-9", - longLink: "https://example.com/longlink-9", - scans: 448, - dateCreated: new Date("2023-08-22T16:55:00") - }, - { - id: 10, - title: "Year-End Clearance Sale", - shortLink: "qr-shortlink-10", - longLink: "https://example.com/longlink-10", - scans: 914, - dateCreated: new Date("2023-07-21T08:30:00") - } -]; export default function QRCodePage(){ - const [loading,setLoading] = useState(false); - const [links,setLinks] = useState([]) + const [loading,setLoading] = useState(false); + const [links,setLinks] = useState([]) useEffect(()=>{ setLoading(true) getLinks().then((res)=>{ - const linkList = res.links; + const linkList:linkType[] | undefined = res.links; setLinks(linkList) setLoading(false) }) @@ -106,6 +23,6 @@ export default function QRCodePage(){ return
- {loading?:
{links.map((qr:any)=>)}
} + {loading?:
{links?.map((qr)=>)}
}
} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 45c636d..25a5302 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -22,7 +22,7 @@ export default function Home() { useEffect(() => { setLoading(true); - const dataString: any = localStorage.getItem("links"); + const dataString = localStorage.getItem("links") as string; parsePublicRecords(dataString).then((s) => { setPublicLinks(s); setLoading(false); @@ -51,7 +51,7 @@ export default function Home() { description: "The link is valid only for 2hrs !!", }); setLongurlInput(""); - updateLocalStorage({ shortUrl: response.shortUrl, longUrl: longurlInput }); + updateLocalStorage({ shortUrl: response.shortUrl as string, longUrl: longurlInput }); }; return ( diff --git a/components/CardComponents/LinkCard.tsx b/components/CardComponents/LinkCard.tsx index 857c4e2..29357e8 100644 --- a/components/CardComponents/LinkCard.tsx +++ b/components/CardComponents/LinkCard.tsx @@ -16,7 +16,8 @@ import { Button } from "../ui/button"; import { EditLinkDialog } from "../DialogComponents/EditLinkDialog"; import { copyToClipboard } from "@/lib/utils"; import { LinkShareDialog } from "../DialogComponents/LinkShareDialog"; -import { LinkType } from "@/interfaces/types"; +import { linkType } from "@/interfaces/types"; +import { useState } from "react"; const months = [ "January", "February", "March", "April", "May", "June", @@ -26,10 +27,12 @@ const months = [ export function LinkCard({ link, }: { - link: any; + link: linkType; }) { - const REDIRECT_URL = process.env.REDIRECT_URL || "https://eurl.vshetty.dev"; - const shortLink:string = `${REDIRECT_URL}/${link.short_code}` + const REDIRECT_URL:string = process.env.REDIRECT_URL || "https://eurl.vshetty.dev"; + const [shortCode,setShortcode] = useState(link.short_code); + const shortLink:string = `${REDIRECT_URL}/${shortCode}` + const [title,setTitle] = useState(link.title) return (
@@ -40,7 +43,7 @@ export function LinkCard({

- {link.title} + {title}

- + - + diff --git a/components/DialogComponents/DatePickerWithRange.tsx b/components/DialogComponents/DatePickerWithRange.tsx index e248876..5a3414e 100644 --- a/components/DialogComponents/DatePickerWithRange.tsx +++ b/components/DialogComponents/DatePickerWithRange.tsx @@ -14,12 +14,11 @@ import { PopoverTrigger, } from "@/components/ui/popover" -export function DatePickerWithRange({date,setDate,current_date}:any) { - // const current_date = new Date() - // const [date, setDate] = React.useState({ - // from: new Date(current_date.getFullYear(), current_date.getMonth(), current_date.getDate()), - // to: addDays(new Date(2022, 0, 20), 20), - // }) +export function DatePickerWithRange({date,setDate,current_date}:{ + date:DateRange | undefined, + current_date:Date|undefined, + setDate: React.Dispatch> +}) { return (
diff --git a/components/DialogComponents/EditLinkDialog.tsx b/components/DialogComponents/EditLinkDialog.tsx index f77cae7..006fa0c 100644 --- a/components/DialogComponents/EditLinkDialog.tsx +++ b/components/DialogComponents/EditLinkDialog.tsx @@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"; import { Dialog, + DialogClose, DialogContent, DialogFooter, DialogHeader, @@ -11,24 +12,60 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { EditLink } from "@/interfaces/types"; +import { updateLinkAction } from "@/lib/actions/updateLinkAction"; +import { HTTP_STATUS } from "@/lib/constants"; import { Lock } from "lucide-react"; import { useState } from "react"; +import { toast } from "../ui/use-toast"; +import { linkType } from "@/interfaces/types"; -export function EditLinkDialog({ children,link }: { +export function EditLinkDialog({ children,link,setShortcode,setParentTitle }: { children:React.ReactNode, - link: EditLink + link: linkType, + setShortcode:React.Dispatch>, + setParentTitle:React.Dispatch> }) { - const [title,setTitle] = useState(link.title) - const [shortLink,setShortLink] = useState(link.shortLink) + const [title,setTitle] = useState(link.title || "") + const [shortLink,setShortLink] = useState(link.short_code) - function editLink(){ - const tempLink = { - title, - shortLink - } - - console.log(tempLink) + const updateLink = async () =>{ + const formdata = new FormData(); + formdata.append('title',title); + formdata.append('short_code',shortLink) + formdata.append('linkId',link.id.toString()) + const new_title = title; + const new_short_code = shortLink; + + const res = await updateLinkAction(formdata) + + if(res.status == HTTP_STATUS.OK){ + toast({ + title:"Link Updated Successfully !!" + }) + + setParentTitle(new_title) + setShortcode(new_short_code) + } + else if(res.status == HTTP_STATUS.CONFLICT){ + toast({ + title:"Conflict!!", + description:"The short code has already been taken", + variant:"destructive" + }) + } + else if(res.status == HTTP_STATUS.BAD_REQUEST){ + toast({ + title:"Invalid Inputs", + variant:"destructive" + }) + } + else{ + toast({ + title:"Some error occured", + description:"Please try again later", + variant:"destructive" + }) + } } return ( @@ -39,8 +76,8 @@ export function EditLinkDialog({ children,link }: { Edit Link
- - setTitle(e.target.value)} className="mt-2" /> + + setTitle(e.target.value)} className="mt-2" />
@@ -51,12 +88,14 @@ export function EditLinkDialog({ children,link }: {
- setShortLink(e.target.value)} className="mt-1" /> + setShortLink(e.target.value)} className="mt-1" />
- + + + diff --git a/components/DialogComponents/LinkShareDialog.tsx b/components/DialogComponents/LinkShareDialog.tsx index 180decc..6e4ec59 100644 --- a/components/DialogComponents/LinkShareDialog.tsx +++ b/components/DialogComponents/LinkShareDialog.tsx @@ -18,12 +18,14 @@ import { WhatsappShareButton } from "react-share"; import { TwitterLogoIcon } from "@radix-ui/react-icons" +import { linkType } from "@/interfaces/types" + export function LinkShareDialog({children,link}:{ children : React.ReactNode, - link:any + link: linkType }) { - const REDIRECT_URL = process.env.REDIRECT_URL || "https://eurl.vshetty.dev"; + const REDIRECT_URL:string = process.env.REDIRECT_URL || "https://eurl.vshetty.dev"; const shortLink:string = `${REDIRECT_URL}/${link.short_code}` function copyToClipboard(){ diff --git a/interfaces/types.ts b/interfaces/types.ts index e1eeb40..413604f 100644 --- a/interfaces/types.ts +++ b/interfaces/types.ts @@ -61,4 +61,13 @@ export type publicLinkType = { longUrl : string; shortUrl : string, clicks? : string +} + +export type linkType = { + id:number, + user_id: number; + short_code: string; + long_url: string; + created_at: Date; + title: string | null; } \ No newline at end of file diff --git a/lib/actions/createLinkAction.ts b/lib/actions/createLinkAction.ts index f3f35fc..00b62bd 100644 --- a/lib/actions/createLinkAction.ts +++ b/lib/actions/createLinkAction.ts @@ -4,7 +4,6 @@ import { createPrivateLink } from "../services/privateLinkManager" export async function createLinkAction (formdata:FormData){ const response = await createPrivateLink(formdata) - console.log(response) return response } diff --git a/lib/actions/createPublicUrl.ts b/lib/actions/createPublicUrl.ts index 5e3e02a..22763b5 100644 --- a/lib/actions/createPublicUrl.ts +++ b/lib/actions/createPublicUrl.ts @@ -5,21 +5,20 @@ import { invokeRedis } from "../services/redisPublicGenerate"; import { urlSchema } from "../zod/url"; const createPublicUrl = async (formData: FormData) => { - const longUrl: any = formData.get("long_url"); + const longUrl: string = formData.get("long_url") as string; - const errors = urlSchema.safeParse({ + const validation = urlSchema.safeParse({ long_url: longUrl, }); - if (!errors.success) { + if (!validation.success) { return { - shortUrl : "", status: HTTP_STATUS.BAD_REQUEST }; } try { - const res = await invokeRedis(longUrl); + const res:string = await invokeRedis(longUrl); return { shortUrl : res, status : HTTP_STATUS.OK @@ -27,9 +26,8 @@ const createPublicUrl = async (formData: FormData) => { ; } catch (e) { return { - shortUrl : "", status : HTTP_STATUS.INTERNAL_SERVER_ERROR - }; + } } }; diff --git a/lib/actions/register.ts b/lib/actions/register.ts index 52bfc79..222586e 100644 --- a/lib/actions/register.ts +++ b/lib/actions/register.ts @@ -17,7 +17,6 @@ const register = async (formData: FormData) => { }, }); - console.log(user); if (user) return 403; diff --git a/lib/actions/updateLinkAction.ts b/lib/actions/updateLinkAction.ts new file mode 100644 index 0000000..99bc981 --- /dev/null +++ b/lib/actions/updateLinkAction.ts @@ -0,0 +1,9 @@ +'use server' + +import { updatePrivateLink } from "../services/privateLinkManager" + +export async function updateLinkAction (formdata:FormData){ + const res = await updatePrivateLink(formdata); + + return res; +} \ No newline at end of file diff --git a/lib/constants.ts b/lib/constants.ts index 4969a97..d19770f 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -6,6 +6,7 @@ export const HTTP_STATUS = { NOT_FOUND: 404, INTERNAL_SERVER_ERROR: 500, FORBIDDEN: 403, + CONFLICT: 409 }; export const ERROR_MESSAGES = { diff --git a/lib/services/privateLinkManager.ts b/lib/services/privateLinkManager.ts index 3db0022..f30d96f 100644 --- a/lib/services/privateLinkManager.ts +++ b/lib/services/privateLinkManager.ts @@ -2,12 +2,15 @@ import { base62_encode } from "@/lib/services/base62"; import incrementCounter from "@/lib/services/counter"; import { getServerSession } from "next-auth"; - +import z from 'zod' import { ISessionType } from "@/interfaces/url"; import authOptions from "@/lib/authOptions"; import validateURLCreateReq from "@/lib/validations/url_create"; import PrismaClientManager from "@/lib/services/pgConnect"; import { HTTP_STATUS } from "@/lib/constants"; +import { linkType } from "@/interfaces/types"; + +const alphabetOnlySchema = z.string().regex(/^[a-zA-Z]+$/); export async function createPrivateLink(formdata : FormData) { @@ -16,6 +19,32 @@ export async function createPrivateLink(formdata : FormData) { const { title,long_url, status } = await validateURLCreateReq(formdata); const session: ISessionType | null = await getServerSession(authOptions); + let custom_short_code:string = formdata.get('short_code') as string + + // if custom short code exists + if(custom_short_code){ + + // check if duplicate + const link: linkType | null = await prisma.links.findFirst({ + where: { + short_code:custom_short_code + } + }) + + if(link) return { + status: HTTP_STATUS.CONFLICT + } + + // checking its regex + const res = alphabetOnlySchema.safeParse(custom_short_code); + + if(!res.success || custom_short_code.startsWith("app")){ + return { + status: HTTP_STATUS.BAD_REQUEST + } + } + } + if (!status) { return { status: HTTP_STATUS.BAD_REQUEST @@ -28,15 +57,17 @@ export async function createPrivateLink(formdata : FormData) { } } - const shortIdLength = await incrementCounter(); - const shortId = base62_encode(shortIdLength); + const shortIdLength:number = await incrementCounter(); + + if(!custom_short_code) + custom_short_code = base62_encode(shortIdLength); try { await prisma.links.create({ data: { title : title as string, long_url: long_url as string, - short_code: shortId, + short_code: custom_short_code, created_at: new Date(), user: { connect: { @@ -46,7 +77,7 @@ export async function createPrivateLink(formdata : FormData) { }, }); return { - short_url: shortId, + short_url: custom_short_code, status: HTTP_STATUS.CREATED } @@ -57,3 +88,70 @@ export async function createPrivateLink(formdata : FormData) { } } + +export async function updatePrivateLink(formdata : FormData){ + const prisma = PrismaClientManager.getInstance().getPrismaClient(); + + const title: string = formdata.get('title') as string + const shortcode: string = formdata.get('short_code') as string + const linkId: number = Number.parseInt(formdata.get('linkId') as string) + + try{ + const link = await prisma.links.findFirst({ + where:{ + short_code:shortcode + } + }) + + if(link && link.id != linkId){ + return {status: HTTP_STATUS.CONFLICT} + } + + await prisma.links.update({ + where:{ + id: linkId + }, + data:{ + title:title, + short_code:shortcode + } + }) + + return { + status: HTTP_STATUS.OK + } + + } + catch(e){ + return { + status: HTTP_STATUS.INTERNAL_SERVER_ERROR + } + } +} + +export async function getTutorialStatus(){ + const prisma = PrismaClientManager.getInstance().getPrismaClient(); + const session: ISessionType | null = await getServerSession(authOptions); + + const TutorialStatus = { + createLink : false, + clickLink : false, + checkAnalytics:false + } + + if (!session?.user) { + return { + status: HTTP_STATUS.UNAUTHORIZED + } + } + + const link:linkType | null = await prisma.links.findFirst({}); + + if(link){ + TutorialStatus.createLink = true; + } + + // If engagement then check + // If checked Analytics thenc check + +} \ No newline at end of file diff --git a/lib/validations/url_create.ts b/lib/validations/url_create.ts index 632e84b..69ec60d 100644 --- a/lib/validations/url_create.ts +++ b/lib/validations/url_create.ts @@ -23,7 +23,6 @@ const validateURLCreateReq = async (formdata: FormData) => { return { title, long_url, status: errors.success, msg: errors.error }; } catch (e) { - console.log(e) return { title: "", long_url: "",