diff --git a/Client/.dockerignore b/Client/.dockerignore new file mode 100644 index 0000000..dd6bcd2 --- /dev/null +++ b/Client/.dockerignore @@ -0,0 +1,7 @@ +## Docker Ignore Files ## + +# 'OS Waste' File +*.DS_Store + +# 'VSCode Waste' File +*.vscode \ No newline at end of file diff --git a/Client/.env.example b/Client/.env.example new file mode 100644 index 0000000..e2806ce --- /dev/null +++ b/Client/.env.example @@ -0,0 +1,14 @@ +## (Example) Environment Variables ## + +# Shortify Server 'Base URL' +NEXT_PUBLIC_SHORTIFY_SERVER_BASE_URL = "Your_Shortify_Server_Base_URL" + +# Clerk 'Service' +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = "Your_Clerk_Publishable_Key" +CLERK_SECRET_KEY = "Your_Clerk_Secret_Key" + +# Clerk 'Routes' +NEXT_PUBLIC_CLERK_SIGN_IN_URL = "/sign-in" +NEXT_PUBLIC_CLERK_SIGN_UP_URL = "/sign-up" +NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL = "/" +NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL = "/" \ No newline at end of file diff --git a/Client/.gitattributes b/Client/.gitattributes index 70894c8..764a6fe 100644 --- a/Client/.gitattributes +++ b/Client/.gitattributes @@ -1,2 +1,4 @@ +## Git Attributes ## + # Auto detect the 'text' files and perform the 'LF' normalization * text=auto diff --git a/Client/Components/Footer/Footer.tsx b/Client/Components/Footer/Footer.tsx deleted file mode 100644 index 9da0a97..0000000 --- a/Client/Components/Footer/Footer.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -import React from 'react'; -import Data from '@/Interface/constant/data'; - -// # Footer Component -const Footer = () => { - return ( -
- Copyright - © - {new Date().getFullYear()} {Data.author} -
- ); -}; - -export default Footer; diff --git a/Client/Dockerfile b/Client/Dockerfile index 1971315..5af5ee0 100644 --- a/Client/Dockerfile +++ b/Client/Dockerfile @@ -1,14 +1,16 @@ -# Use the official Nginx image as the base image +## DockerFile: For "Nginx" Application ## + +# Start from the latest "nginx" base image (alpine) FROM nginx:alpine -# Remove the default Nginx configuration file +# Remove the default "Nginx" configuration 'file' RUN rm /usr/share/nginx/html/index.html -# Copy the custom index.html to the Nginx web root +# Copy the custom 'index.html' to the "Nginx" web 'root' COPY index.html /usr/share/nginx/html/ -# Expose port 80 to access the web page +# Expose the port "80" to 'access' the web 'page' EXPOSE 80 -# Start Nginx server +# Start the "Nginx" server CMD ["nginx", "-g", "daemon off;"] diff --git a/Client/Interface/constant/data.ts b/Client/Interface/constant/data.js similarity index 58% rename from Client/Interface/constant/data.ts rename to Client/Interface/constant/data.js index 7d58645..b7b6594 100644 --- a/Client/Interface/constant/data.ts +++ b/Client/Interface/constant/data.js @@ -1,7 +1,5 @@ -import { DataProp } from '../types/data'; - // # 'Data' Constant -const Data: DataProp = { +const Data = { name: "Suraj Dalvi", author: "Suraj Dalvi" }; diff --git a/Client/Interface/constant/metadata.ts b/Client/Interface/constant/metadata.js similarity index 59% rename from Client/Interface/constant/metadata.ts rename to Client/Interface/constant/metadata.js index 5112987..6ff6b1e 100644 --- a/Client/Interface/constant/metadata.ts +++ b/Client/Interface/constant/metadata.js @@ -1,10 +1,8 @@ -import { MetaDataProp } from '../types/metadata'; - // # 'MetaData' Constant -const MetaData: MetaDataProp = { +const MetaData = { title: "Shortify - URL Shortener", description: "💲Shortify💲 ~ 🕸️ URL Shortener 🕸️", - icons: "/Logo.png" + icons: "logo.png" }; export default MetaData; \ No newline at end of file diff --git a/Client/Interface/constant/ui.js b/Client/Interface/constant/ui.js new file mode 100644 index 0000000..49d331a --- /dev/null +++ b/Client/Interface/constant/ui.js @@ -0,0 +1,8 @@ +// # 'UI Data' Constant +const UIData = { + site_name: "Shortify", + black_logo: "black_logo.png", // # For "Light" Mode + white_logo: "white_logo.png" // # For "Dark" Mode +}; + +export default UIData; \ No newline at end of file diff --git a/Client/Interface/types/data.ts b/Client/Interface/types/data.ts deleted file mode 100644 index 5ca2666..0000000 --- a/Client/Interface/types/data.ts +++ /dev/null @@ -1,7 +0,0 @@ -// # 'Data' Type -type DataProp = { - name: string; - author: string; -}; - -export type { DataProp }; diff --git a/Client/Interface/types/metadata.ts b/Client/Interface/types/metadata.ts deleted file mode 100644 index a861195..0000000 --- a/Client/Interface/types/metadata.ts +++ /dev/null @@ -1,8 +0,0 @@ -// # 'MetaData' Type -type MetaDataProp = { - title: string; - description: string; - icons: string; -}; - -export type { MetaDataProp }; \ No newline at end of file diff --git a/Client/LICENCE b/Client/LICENCE index dd1ba19..7ef950a 100644 --- a/Client/LICENCE +++ b/Client/LICENCE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Suraj Dalvi +Copyright (c) 2026 Suraj Dalvi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Client/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx b/Client/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.jsx similarity index 100% rename from Client/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx rename to Client/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.jsx diff --git a/Client/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx b/Client/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.jsx similarity index 100% rename from Client/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx rename to Client/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.jsx diff --git a/Client/app/(auth)/layout.tsx b/Client/app/(auth)/layout.jsx similarity index 69% rename from Client/app/(auth)/layout.tsx rename to Client/app/(auth)/layout.jsx index 614811f..b3a5c6d 100644 --- a/Client/app/(auth)/layout.tsx +++ b/Client/app/(auth)/layout.jsx @@ -1,4 +1,4 @@ -const AuthLayout = ({ children }: { children: React.ReactNode }) => { +const AuthLayout = ({ children }) => { return ( <>
diff --git a/Client/app/contact/page.tsx b/Client/app/contact/page.tsx deleted file mode 100644 index 53a4a72..0000000 --- a/Client/app/contact/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; -import Footer from '@/Components/Footer/Footer'; - - - -// # Contact Page -const page = () => { - return ( -
-
-
- ); -}; - -export default page; diff --git a/Client/app/globals.css b/Client/app/globals.css index 4d7f47f..c9cc230 100644 --- a/Client/app/globals.css +++ b/Client/app/globals.css @@ -1,31 +1,71 @@ -/* ## App Global CSS ## */ +/* ## App "Global" CSS ## */ -/* # Tailwind CSS # */ +/* # "Tailwind" CSS # */ @tailwind base; @tailwind components; @tailwind utilities; -/* # Scrollbar CSS # */ +/* # "Layer" CSS # */ +@layer base { -/* Firefox */ -* { - scrollbar-width: auto; - scrollbar-color: #5b8be0 #ffffff; -} + /* # "Light" Theme # */ + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } -/* Chrome | Edge | Safari */ -*::-webkit-scrollbar { - width: 12px; - height: 20px; + /* # "Dark" Theme # */ + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } } -*::-webkit-scrollbar-track { - background: transparent; - margin: 10px; -} +/* # "Layer" CSS # */ +@layer base { + + /* # "Border" CSS # */ + * { + @apply border-border; + } -*::-webkit-scrollbar-thumb { - background-color: #5b8be0; - border-radius: 10px; - border: 3px solid black; + /* # "Body" CSS # */ + body { + @apply bg-background text-foreground; + } } \ No newline at end of file diff --git a/Client/app/layout.jsx b/Client/app/layout.jsx new file mode 100644 index 0000000..1a42f1d --- /dev/null +++ b/Client/app/layout.jsx @@ -0,0 +1,22 @@ +import './globals.css' +import MetaData from '../Interface/constant/metadata'; +import { ClerkProvider } from '@clerk/nextjs'; +import "react-toastify/dist/ReactToastify.css"; + +export const metadata = MetaData + +const RootLayout = ({ children }) => { + return ( + <> + + + + {children} + + + + + ); +}; + +export default RootLayout; \ No newline at end of file diff --git a/Client/app/layout.tsx b/Client/app/layout.tsx deleted file mode 100644 index 1f75169..0000000 --- a/Client/app/layout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import './globals.css' -import { Inter } from 'next/font/google' -import { ToastContainer } from "react-toastify"; -import MetaData from '@/Interface/constant/metadata'; -import { ClerkProvider } from '@clerk/nextjs'; -import "react-toastify/dist/ReactToastify.css"; - -const inter = Inter({ subsets: ['latin'] }) - -export const metadata = MetaData - -export default function RootLayout({ - children -}: { - children: React.ReactNode -}) { - return ( - - - - {children} - - - - - ) -} diff --git a/Client/app/page.jsx b/Client/app/page.jsx new file mode 100644 index 0000000..4f3c8d0 --- /dev/null +++ b/Client/app/page.jsx @@ -0,0 +1,232 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useUser } from '@clerk/nextjs'; +import { Plus } from 'lucide-react'; +import { Button } from '../components/ui/button'; +import { Card, CardContent } from '../components/ui/card'; +import Navbar from '../components/Navbar'; +import Footer from '../components/Footer'; +import LinkCard from '../components/LinkCard'; +import CreateLinkDialog from '../components/CreateLinkDialog'; +import LinkDetailDialog from '../components/LinkDetailDialog'; +import EditUsernameDialog from '../components/EditUsernameDialog'; +import Toast from '../components/Toast'; +import { userAPI, linkAPI } from '@/lib/api'; +import { getSuccessMsg } from '@/lib/success'; + +const Dashboard = () => { + const { user, isLoaded } = useUser(); + const [links, setLinks] = useState([]); + const [selectedLink, setSelectedLink] = useState(null); + const [userData, setUserData] = useState(null); + const [isCreateOpen, setIsCreateOpen] = useState(false); + const [isEditUserOpen, setIsEditUserOpen] = useState(false); + const [isDetailOpen, setIsDetailOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [toast, setToast] = useState(null); + + useEffect(() => { + if (user) { + fetchUser(); + fetchLinks(); + } + }, [user]); + + const showToast = (message, type) => { + setToast({ message, type }); + }; + + const fetchUser = async () => { + try { + const response = await userAPI.getUser(user.id); + if (response.success) { + setUserData(response.payload); + } + } catch (err) { + showToast(err.message, 'error'); + } + }; + + const fetchLinks = async () => { + setLoading(true); + try { + const response = await linkAPI.getLinks(user.id); + if (response.success) { + setLinks(response.payload); + } + } catch (err) { + showToast(err.message, 'error'); + } finally { + setLoading(false); + } + }; + + const fetchLinkDetails = async (link) => { + try { + const response = await linkAPI.getLink(link._id); + if (response.success) { + setSelectedLink(response.payload); + setIsDetailOpen(true); + } + } catch (err) { + showToast(err.message, 'error'); + } + }; + + const createLink = async (data) => { + try { + const response = await linkAPI.createLink(user.id, data); + if (response.success) { + const successMsg = getSuccessMsg(response.payload); + showToast(successMsg, 'success'); + setIsCreateOpen(false); + fetchLinks(); + } + } catch (err) { + showToast(err.message, 'error'); + setIsCreateOpen(false); + } + }; + + const updateLink = async (linkId, data) => { + try { + const response = await linkAPI.updateLink(user.id, linkId, data); + if (response.success) { + const successMsg = getSuccessMsg(response.payload); + showToast(successMsg, 'success'); + setIsDetailOpen(false); + fetchLinks(); + } + } catch (err) { + showToast(err.message, 'error'); + setIsDetailOpen(false); + } + }; + + const deleteLink = async (linkId) => { + if (!confirm('Are you sure you want to delete this link?')) return; + try { + const response = await linkAPI.deleteLink(linkId); + if (response.success) { + const successMsg = getSuccessMsg(response.payload); + showToast(successMsg, 'success'); + fetchLinks(); + } + } catch (err) { + showToast(err.message, 'error'); + fetchLinks(); + } + }; + + const updateUsername = async (username) => { + try { + const response = await userAPI.updateUsername(user.id, username); + if (response.success) { + const successMsg = getSuccessMsg(response.payload); + showToast(successMsg, 'success'); + setIsEditUserOpen(false); + fetchUser(); + } + } catch (err) { + showToast(err.message, 'error'); + setIsEditUserOpen(false); + } + }; + + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text); + showToast('Copied to clipboard!', 'success'); + }; + + if (!isLoaded) { + return ( +
+
+
+ ); + } + + return ( + <> +
+ setIsEditUserOpen(true)} + /> + +
+
+
+

Link Management

+

Create and manage your shortened URLs

+
+ +
+ + {loading ? ( +
+
+
+ ) : !links || links.length === 0 ? ( + + + + + +

No links yet

+

Create your first shortened URL to get started

+ +
+
+ ) : ( +
+ {links.map((link) => ( + + ))} +
+ )} + + + +
+ +
+ + {toast && ( + setToast(null)} + /> + )} +
+ + ); +}; + +export default Dashboard; diff --git a/Client/app/page.tsx b/Client/app/page.tsx deleted file mode 100644 index a128a1e..0000000 --- a/Client/app/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -"use client"; - -import React from "react"; -import Footer from '@/Components/Footer/Footer'; - -// # Shortify Page - Entry Point -const page = () => { - return ( - <> -