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

Major feature additions #6

Merged
merged 1 commit into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "ShortendUrl" (
"id" TEXT NOT NULL,
"shortUrl" TEXT NOT NULL,
"longUrl" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ShortendUrl_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "ShortendUrl_shortUrl_key" ON "ShortendUrl"("shortUrl");

-- CreateIndex
CREATE INDEX "ShortendUrl_shortUrl_idx" ON "ShortendUrl"("shortUrl");

-- AddForeignKey
ALTER TABLE "ShortendUrl" ADD CONSTRAINT "ShortendUrl_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- A unique constraint covering the columns `[longUrl]` on the table `ShortendUrl` will be added. If there are existing duplicate values, this will fail.

*/
-- CreateIndex
CREATE UNIQUE INDEX "ShortendUrl_longUrl_key" ON "ShortendUrl"("longUrl");
46 changes: 29 additions & 17 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@

generator client {
provider = "prisma-client-js"
output = "../node_modules/.prisma/client"
output = "../node_modules/.prisma/client"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
provider = "postgresql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // Only needed when using a cloud provider that doesn't support the creation of new databases, like Heroku. Learn more: https://pris.ly/d/migrate-shadow
}

model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

Expand All @@ -40,14 +40,15 @@ model Session {
}

model User {
id String @id @default(cuid())
id String @id @default(cuid())
name String?
email String? @unique
email String? @unique
password String?
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
ShortendUrl ShortendUrl[]
}

model VerificationToken {
Expand All @@ -57,3 +58,14 @@ model VerificationToken {

@@unique([identifier, token])
}

model ShortendUrl {
id String @id @default(cuid())
shortUrl String @unique
longUrl String @unique
userId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@index([shortUrl])
}
1 change: 0 additions & 1 deletion src/app/(auth)/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const SignInPage = () => {
email,
password,
callbackUrl: '/',
redirect: false,
})

setTimeout(() => {
Expand Down
4 changes: 2 additions & 2 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { Icons } from '@/components/icons'
import { SubmitHandler, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { signUpSchema as authSchema } from '@/common'
import { useMutation } from '@/common/make-request'
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { UserCheck } from 'lucide-react'
import { useMutation } from '@/common/make-request'

const signUpSchema = authSchema
.extend({
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/auth/signup/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const POST = async (request: Request) => {
} catch (error) {
let message = ''
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P202') {
if (error.code === 'P2002') {
message = 'User already exists.'
}
} else {
Expand Down
70 changes: 70 additions & 0 deletions src/app/api/shorten/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import prisma from '@/common/db'
import { Prisma } from '@prisma/client'
import { randomUUID } from 'crypto'
import z from 'zod'

const shortendUrlSchema = z.object({
url: z.string().url(),
userId: z.string().cuid(),
})

const urlShorterAlgorithm = (url: string): string => {
const baseUrl = process.env.BASE_URL!
const encodedString = btoa(url)
const encodedStrLen = encodedString.length
const uuid = randomUUID()

const newEncodedString =
encodedString[0] +
uuid.slice(4, 6) +
encodedString.slice(encodedStrLen - 1) +
encodedString[encodedStrLen / 2]

return `${baseUrl}/${newEncodedString}`
}

export const POST = async (request: Request) => {
const { url, userId } = await request.json()
const validatedData = shortendUrlSchema.safeParse({
url,
userId,
})

if (!validatedData.success) {
return Response.json('Invalid payload', {
status: 400,
})
}

let result: Prisma.ShortendUrlUncheckedCreateInput

try {
const shortUrl = urlShorterAlgorithm(url)
result = await prisma.shortendUrl.create({
data: {
userId: userId,
shortUrl,
longUrl: url,
},
})
} catch (error) {
let message = ''
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P202') {
message = 'This url has already been shortend'
}
} else {
message = error.message
}
return Response.json(message, {
status: 400,
})
}

return Response.json(
{ message: 'Url shortend Successfully', data: result },
{
status: 200,
}
)
}
50 changes: 50 additions & 0 deletions src/app/api/shortend-urls/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import prisma from '@/common/db'
import { z } from 'zod'

const deletePayloadSchema = z.object({
userId: z.string().cuid(),
shortendUrlIds: z.string().array(),
})

export const POST = async (req: Request) => {
const { shortendUrlIds, userId } = await req.json()

const validatedData = deletePayloadSchema.safeParse({
shortendUrlIds,
userId,
})

if (!validatedData.success) {
return Response.json('Invalid payload', {
status: 400,
})
}

try {
const result = await prisma.shortendUrl.deleteMany({
where: {
userId,
id: { in: shortendUrlIds },
},
})

let message = ''
if (result.count > 0) {
message = 'Url bulk deleted succesfully'
} else {
message = 'No Url found!'
}

return Response.json(
{ message, data: result },
{
status: 200,
}
)
} catch (error) {
const message = error.message
return Response.json(message, {
status: 400,
})
}
}
24 changes: 24 additions & 0 deletions src/app/api/users/[id]/shortend-urls/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import prisma from '@/common/db'

export const GET = async (_, context: { params }) => {
const { id } = context.params
try {
const result = await prisma.shortendUrl.findMany({
where: {
userId: id,
},
})

return Response.json(
{ message: 'Shortend url list fetched successfully', data: result },
{
status: 400,
}
)
} catch (error) {
const message = error.message
return Response.json(message, {
status: 400,
})
}
}
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {

return (
<html lang="en">
<body>
<body className="w-screen h-screen">
<ClientSessionProvider session={session}>
<ThemeProvider
attribute="class"
Expand Down
8 changes: 7 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { HomePage } from '@/components/homepage'

const RootPage = () => {
return <> </>
return (
<div className="w-full h-full">
<HomePage />
</div>
)
}

export default RootPage
1 change: 0 additions & 1 deletion src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './provider'
export * from './validation'
export * from './make-request'
23 changes: 23 additions & 0 deletions src/components/homepage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'
import { ScissorsLineDashed } from 'lucide-react'
import { Button, Input } from './ui'

const InputURL = () => {
return (
<div className="flex w-full items-center justify-center space-x-2">
<Input type="text" placeholder="Enter your long URL here.." />
<Button type="submit" className="gap-2">
<ScissorsLineDashed /> <span> Shorten </span>
</Button>
</div>
)
}

export const HomePage = () => {
return (
<div className="h-full w-full flex flex-col items-center justify-center ">
<h1> Shorty Url - The Fast and Reliable URL Shortener </h1>
<InputURL />
</div>
)
}
2 changes: 1 addition & 1 deletion src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-15 w-6/12 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
Expand Down