feature-page (#11,#4)

Refactor URL Shortening Feature and UI Enhancements
* Adds new feature page, user interface and user interface enhancement
Co-authored-by: anthony <anthony@Project42 - SCM <noreply@scm.project42.io>>
Co-committed-by: anthony <anthony@Project42 - SCM <noreply@scm.project42.io>>
This commit is contained in:
anthony 2024-12-20 22:01:07 +00:00 committed by Elia El Lazkani
parent 11e9ee6a32
commit 7f813e2c34
17 changed files with 577 additions and 290 deletions

View file

@ -16,9 +16,10 @@
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"axios": "^1.7.9", "axios": "^1.7.9",
"framer-motion": "^11.15.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-icons": "^5.3.0", "react-icons": "^5.4.0",
"react-router-dom": "^7.0.1", "react-router-dom": "^7.0.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-toastify": "^11.0.1", "react-toastify": "^11.0.1",
@ -7850,6 +7851,32 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/framer-motion": {
"version": "11.15.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz",
"integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
"dependencies": {
"motion-dom": "^11.14.3",
"motion-utils": "^11.14.3",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fresh": { "node_modules/fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -10668,6 +10695,16 @@
"mkdirp": "bin/cmd.js" "mkdirp": "bin/cmd.js"
} }
}, },
"node_modules/motion-dom": {
"version": "11.14.3",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
"integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA=="
},
"node_modules/motion-utils": {
"version": "11.14.3",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
"integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ=="
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -12928,9 +12965,9 @@
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
}, },
"node_modules/react-icons": { "node_modules/react-icons": {
"version": "5.3.0", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
"integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
"peerDependencies": { "peerDependencies": {
"react": "*" "react": "*"
} }

View file

@ -11,9 +11,10 @@
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"axios": "^1.7.9", "axios": "^1.7.9",
"framer-motion": "^11.15.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-icons": "^5.3.0", "react-icons": "^5.4.0",
"react-router-dom": "^7.0.1", "react-router-dom": "^7.0.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-toastify": "^11.0.1", "react-toastify": "^11.0.1",

View file

@ -1,25 +1,13 @@
import React from "react"; import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router } from "react-router-dom";
import "./App.css"; import "./App.css";
import AppContent from "./AppContent";
// Pages & Components
import Navbar from "./components/navbar/Navbar";
import Footer from "./components/footer/Footer";
import Home from "./pages/home/Home";
import About from "./pages/about/About";
import Contact from "./pages/contact/Contact";
function App() { function App() {
return ( return (
<div className="App"> <div className="App">
<Router> <Router>
<Navbar /> <AppContent />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
<Footer />
</Router> </Router>
</div> </div>
); );

View file

@ -0,0 +1,26 @@
import { Routes, Route, useLocation } from "react-router-dom";
import "./App.css";
import { AnimatePresence } from "framer-motion";
// Pages & Components
import Navbar from "./components/navbar/Navbar";
import Footer from "./components/footer/Footer";
import Home from "./pages/home/Home";
import Features from "./pages/features/Features";
import Contact from "./pages/contact/Contact";
export default function AppContent() {
const location = useLocation();
return (
<>
<Navbar />
<AnimatePresence mode="wait" />
<Routes location={location} key={location.pathname}>
<Route path="/" element={<Home />} />
<Route path="/features" element={<Features />} />
<Route path="/contact" element={<Contact />} />
</Routes>
<Footer />
</>
);
}

View file

@ -0,0 +1,155 @@
.URLShortener-component {
height: 300px;
width: 800px;
border-radius: 5px;
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.1);
display: grid;
grid-template-columns: 2fr 1fr;
transition: box-shadow 0.3s ease-in-out;
}
.URLShortener-component:hover {
box-shadow: 0px 20px 25px rgba(0, 0, 0, 0.3), 0px 8px 10px rgba(0, 0, 0, 0.2);
}
.left-side {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 10px;
padding-bottom: 20%;
padding-left: 5%;
padding-right: 5%;
}
.left-side h1 {
color: var(--color-tertiary);
border-bottom: 1px solid var(--color-tertiary);
}
.url-input {
display: flex;
gap: 2px;
}
.url-input input {
width: 250px;
height: 40px;
outline: none;
border-radius: 5px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border: 1px solid rgba(0, 0, 0, 0.4);
padding-left: 5px;
white-space: nowrap;
}
.url-input button {
border-radius: 5px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background-color: var(--color-tertiary);
border: none;
min-width: 80px;
color: #fff;
transition: 0.2s;
}
.url-input button:hover {
opacity: 0.8;
}
.right-side {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
border-left: 2px solid rgba(0, 0, 0, 0.1);
padding: 0 5%;
}
.right-side-title h1 {
color: var(--color-tertiary);
}
.right-side-content p {
font-family: "Roboto", sans-serif;
font-size: 14px;
color: #808080;
}
@media (max-width: 730px) {
.URLShortener-component {
grid-template-columns: 1fr;
height: auto;
width: auto;
}
.left-side {
padding-bottom: 5%;
}
.right-side {
border-left: none;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
}
@media (max-width: 475px) {
.url-input input {
height: 25px;
width: 200px;
}
.left-side h1 {
font-size: 25px;
}
.right-side-title h1 {
font-size: 25px;
}
.right-side-content p {
font-size: 13px;
}
.url-input button {
min-width: 60px;
}
}
@media (max-width: 475px) {
.url-input {
flex-direction: column;
}
.url-input button {
height: 30px;
border-radius: 5px;
}
.url-input input {
border-radius: 5px;
}
}
.custom-toast {
font-family: "Roboto", sans-serif;
font-size: 13px;
}
@keyframes fadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.url-input.shortened {
opacity: 0;
transform: translateY(-20px);
animation: fadeIn 0.3s forwards;
}

View file

@ -0,0 +1,112 @@
import React, { useState } from "react";
import "./URLShortener.css";
import axios from "axios";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
export default function () {
const [url, setUrl] = useState<string>("");
const [shortenedUrl, setShortenedUrl] = useState<string>("");
const [showInput, setShowInput] = useState<boolean>(false);
async function ShortenIt(e: React.FormEvent) {
e.preventDefault();
if (!url) {
toast.error("Please provide a URL", {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "dark",
className: "custom-toast",
});
return;
}
try {
const timestamp = Math.floor(Date.now() / 1000);
// Send the POST request to the backend
await axios
.post("http://127.0.0.1:8000/shortenit", {
url: url,
timestamp: timestamp,
})
.then((response) => {
if (response) {
const code: string = response.data.url;
const fullShortenedUrl: string = `http://127.0.0.1:8000/${code}`;
setShortenedUrl(fullShortenedUrl);
setShowInput(true);
}
});
setTimeout(() => {
setShowInput(false);
}, 10000);
} catch (error) {
console.log(error);
}
}
async function copyURL() {
try {
if (shortenedUrl) {
await navigator.clipboard.writeText(shortenedUrl);
toast.success("Copied to Clipboard", {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "dark",
className: "custom-toast",
});
}
} catch (error) {
console.log(error);
}
}
return (
<div className="URLShortener-component">
<div className="left-side">
<h1>Paste the URL</h1>
<form className="url-input" onSubmit={ShortenIt}>
<input
type="text"
placeholder="www.example.com"
aria-label="URL input field"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<button type="submit">Shorten It</button>
</form>
{shortenedUrl && showInput && (
<div className="url-input shortened">
<input type="text" readOnly value={shortenedUrl} />
<button onClick={copyURL}>Copy</button>
</div>
)}
</div>
<div className="right-side">
<div className="right-side-title">
<h1>Shorten It</h1>
</div>
<div className="right-side-content">
<p>
Shortenit is a free and open-source URL shortening service designed
for simplicity and efficiency.
</p>
</div>
</div>
<ToastContainer />
</div>
);
}

View file

@ -0,0 +1,8 @@
import React from "react";
export interface CardProps {
icon: React.ReactNode;
title: string;
description: string;
link : string;
}

View file

@ -0,0 +1,64 @@
.Card {
position: relative;
height: 270px;
width: 250px;
border-radius: 10px;
display: grid;
grid-template-rows: repeat(2, 1fr);
overflow: hidden;
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.15), 0px 4px 6px rgba(0, 0, 0, 0.1);
font-family: "Roboto", sans-serif;
padding: 15px;
transition: .3s, transform .6s ease;
}
.Card:hover {
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.4), 0px 4px 6px rgba(0, 0, 0, 0.2);
transform: translateY(-25px);
}
.top-part {
display: flex;
justify-content: center;
align-items: center;
}
.feature-icon {
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
color: var(--color-primary);
height: 60px;
width: 60px;
border-radius: 50%;
background-color: var(--color-tertiary);
}
.bottom-part {
text-align: center;
}
.feature-description {
color: #666;
font-size: 13px;
}
.feature-button button {
padding: 8px 30px;
border-radius: 5px;
background-color: #1d4ed8;
border: 1px solid #f5f5f5;
color: #fff;
border: none;
cursor: pointer;
transition: 0.3s;
}
.feature-button button a {
color: #fff;
}
.feature-button button:hover {
opacity: 0.8;
}

View file

@ -0,0 +1,38 @@
import React from "react";
import "./FeaturesCard.css";
import { Link, useNavigate } from "react-router-dom";
import { CardProps } from "./CardProps";
const FeaturesCard: React.FC<CardProps> = ({
icon,
title,
description,
link,
}) => {
const navigate = useNavigate();
return (
<div className="Card" onClick={() => navigate(`${link}`)}>
<div className="top-part">
<div className="feature-icon">{icon}</div>
</div>
<div className="bottom-part">
<div className="feature-title">
<h3>{title}</h3>
</div>
<div className="feature-description">
<p>{description}</p>
</div>
<div className="feature-button">
<button>
<Link to={link}>Read More</Link>
</button>
</div>
</div>
</div>
);
};
export default FeaturesCard;

View file

@ -4,6 +4,9 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 20px; padding: 0 20px;
position: sticky;
top: 0;
z-index: 1000;
} }
.logo a { .logo a {

View file

@ -23,7 +23,7 @@ function Navbar() {
<Link to="/">Home</Link> <Link to="/">Home</Link>
</li> </li>
<li> <li>
<Link to="/about">About</Link> <Link to="/features">Features</Link>
</li> </li>
<li> <li>
<Link to="/contact">Contact</Link> <Link to="/contact">Contact</Link>

View file

@ -1,8 +0,0 @@
import React from "react";
import "./About.css";
function About() {
return <h1>ABOUT PAGE</h1>;
}
export default About;

View file

@ -0,0 +1,70 @@
.features-header {
width: 100%;
text-align: center;
font-family: "Roboto", sans-serif;
background-color: var(--color-primary);
margin-bottom: 75px;
}
.features-header h2 {
font-size: 25px;
color: var(--color-tertiary);
}
.features-header h1 {
font-size: 35px;
}
.features-body {
min-height: 100vh;
margin: 0 10%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-row-gap: 20px;
}
@media (max-width: 1466px) {
.features-body {
display: grid;
grid-template-columns: repeat(3, 1fr);
place-items: center;
}
}
@media (max-width: 1076px) {
.features-body {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 708px) {
.features-body {
display: grid;
grid-template-columns: repeat(1, 1fr);
grid-row-gap: 20px;
}
}
@media (max-width: 390px) {
.features-body {
display: flex;
gap: 1em;
overflow-x: scroll;
scroll-snap-type: x mandatory;
}
.features-header {
margin-bottom: -50%;
}
.Feature-Card {
flex-shrink: 0;
aspect-ratio: 3/2;
scroll-snap-align: start;
}
.features-header h1 {
font-size: 25px;
}
}

View file

@ -0,0 +1,41 @@
import React from "react";
import "./Features.css";
import FeaturesCard from "../../components/featuresCard/FeaturesCard";
import { FaLink } from "react-icons/fa";
import { motion } from "framer-motion";
function Features() {
return (
<>
<motion.section
className="features-header"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.7, ease: "easeInOut" }}
>
<h2>Features</h2>
<h1>Our Services & Features</h1>
</motion.section>
<motion.section
className="features-body"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 1.5, ease: "easeInOut" }}
>
<div className="Feature-Card">
<FeaturesCard
icon={<FaLink />}
title="URL Shortener"
description="Transform long URLs into short, shareable links in a blink of an eye."
link="/"
/>
</div>
</motion.section>
</>
);
}
export default Features;

View file

@ -6,161 +6,3 @@
justify-content: center; justify-content: center;
margin: auto; margin: auto;
} }
.home-container {
height: 300px;
width: 800px;
border-radius: 5px;
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.1);
display: grid;
grid-template-columns: 2fr 1fr;
transition: box-shadow 0.3s ease-in-out;
}
.home-container:hover {
box-shadow: 0px 20px 25px rgba(0, 0, 0, 0.3), 0px 8px 10px rgba(0, 0, 0, 0.2);
}
.left-side {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 10px;
padding-bottom: 20%;
padding-left: 5%;
padding-right: 5%;
}
.left-side h1 {
color: var(--color-tertiary);
border-bottom: 1px solid var(--color-tertiary);
}
.url-input {
display: flex;
gap: 2px;
}
.url-input input {
/* position: relative; */
width: 250px;
height: 40px;
outline: none;
border-radius: 5px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border: 1px solid rgba(0, 0, 0, 0.4);
padding-left: 5px;
white-space: nowrap;
}
.url-input button {
border-radius: 5px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background-color: var(--color-tertiary);
border: none;
min-width: 80px;
color: #fff;
transition: 0.2s;
}
.url-input button:hover {
opacity: 0.8;
}
.right-side {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
border-left: 2px solid rgba(0, 0, 0, 0.1);
padding: 0 5%;
}
.right-side-title h1 {
color: var(--color-tertiary);
}
.right-side-content p {
font-family: "Roboto", sans-serif;
font-size: 14px;
/* color: var(--text-color); */
color: #808080;
}
@media (max-width: 730px) {
.home-container {
grid-template-columns: 1fr;
height: auto;
width: auto;
}
.left-side {
padding-bottom: 5%;
}
.right-side {
border-left: none;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
}
@media (max-width: 475px) {
.url-input input {
height: 25px;
width: 200px;
}
.left-side h1 {
font-size: 25px;
}
.right-side-title h1 {
font-size: 25px;
}
.right-side-content p {
font-size: 13px;
}
.url-input button {
min-width: 60px;
}
}
@media (max-width: 475px) {
.url-input {
flex-direction: column;
}
.url-input button {
height: 30px;
border-radius: 5px;
}
.url-input input {
border-radius: 5px;
}
}
.custom-toast {
font-family: "Roboto", "sans-serif";
font-size: 13px;
}
@keyframes fadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.url-input.shortened {
opacity: 0;
transform: translateY(-20px);
animation: fadeIn 0.3s forwards;
}

View file

@ -1,110 +1,20 @@
import React, { FormEvent, useState, useEffect } from "react"; import React from "react";
import "./Home.css"; import "./Home.css";
import axios from "axios"; import { motion } from "framer-motion";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import URLShortener from "../../components/URLShortener/URLShortener";
function Home() { function Home() {
const [url, setUrl] = useState<string>("");
const [shortenedUrl, setShortenedUrl] = useState<string>("");
async function ShortenIt(e: React.FormEvent) {
e.preventDefault();
if (!url) {
toast.error("Please provide a URL", {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "dark",
className: "custom-toast",
});
return;
}
try {
const timestamp = Math.floor(Date.now() / 1000);
// Send the POST request to the backend
await axios
.post("http://127.0.0.1:8000/shortenit", {
url: url,
timestamp: timestamp,
})
.then((response) => {
if (response) {
const code: string = response.data.url;
const fullShortenedUrl: string = `http://127.0.0.1:8000/${code}`;
setShortenedUrl(fullShortenedUrl);
}
});
} catch (error) {
console.log(error);
}
}
async function copyURL() {
try {
if (shortenedUrl) {
await navigator.clipboard.writeText(shortenedUrl);
toast.success("Copied to Clipboard", {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "dark",
className: "custom-toast",
});
}
} catch (error) {
console.log(error);
}
}
return ( return (
<div className="home-page"> <motion.div
<div className="home-container"> className="home-page"
<div className="left-side"> initial={{ opacity: 0 }}
<h1>Paste the URL</h1> animate={{ opacity: 1 }}
<form className="url-input" onSubmit={ShortenIt}> exit={{ opacity: 0 }}
<input transition={{ duration: 1.5, ease: "easeInOut" }}
type="text" >
placeholder="www.example.com" <URLShortener />
aria-label="URL input field" </motion.div>
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<button type="submit">Shorten It</button>
</form>
{shortenedUrl && (
<div className="url-input shortened">
<input type="text" readOnly value={shortenedUrl} />
<button onClick={copyURL}>Copy</button>
</div>
)}
</div>
<div className="right-side">
<div className="right-side-title">
<h1>Shorten It</h1>
</div>
<div className="right-side-content">
<p>
Shortenit is a free and open-source URL shortening service
designed for simplicity and efficiency.
</p>
</div>
</div>
</div>
<ToastContainer />
</div>
); );
} }