Nest intial code
This commit is contained in:
commit
7b299ae5b5
8 changed files with 2217 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
todo.txt
|
76
README.md
Normal file
76
README.md
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
# SaifURL
|
||||||
|
|
||||||
|
Welcome to **SaifURL**, my personal URL shortening service! 🚀
|
||||||
|
|
||||||
|
## About the Project
|
||||||
|
|
||||||
|
SaifURL is a custom-built tool designed exclusively for my own use. It helps me shorten long and complex URLs into concise, manageable links. This service is tailored specifically to my needs, ensuring a streamlined and efficient experience.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Custom Short Links**: Quickly generate short, personal URLs for easier sharing and organization.
|
||||||
|
- **Simple Design**: A clean and intuitive interface designed just for me.
|
||||||
|
- **Fast and Reliable**: No delays—shorten URLs instantly.
|
||||||
|
- **Private Service**: This tool is solely for my personal use and not intended for public access.
|
||||||
|
|
||||||
|
### Built With
|
||||||
|
|
||||||
|
This project leverages the following technologies:
|
||||||
|
|
||||||
|
- **EJS** (Embedded JavaScript): Templating engine for dynamic HTML rendering.
|
||||||
|
- **JavaScript**: Core functionality and logic.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Since this service is strictly for my personal use, here’s how I set it up for myself:
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Ensure the following are installed on my system:
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) (Latest LTS version recommended)
|
||||||
|
- [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/saifabdelrazek011/saifurl.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Navigate to the project directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd saifurl
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
1. Start the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Open my browser and visit:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use the service to shorten my URLs as needed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the [MIT License](LICENSE). Since this is a personal tool, I control its usage and modifications.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Let me know if you'd like any further changes!
|
27
modules/shortUrl.js
Normal file
27
modules/shortUrl.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import mongoose from "mongoose";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
const shortUrlSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
full: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
short: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
default: () => nanoid(6),
|
||||||
|
},
|
||||||
|
clicks: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const ShortUrl = mongoose.model("ShortUrl", shortUrlSchema);
|
||||||
|
export default ShortUrl;
|
1609
package-lock.json
generated
Normal file
1609
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
33
package.json
Normal file
33
package.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "saifurl",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"homepage": "https://github.com/SaifAbdelrazek011/saifurl#readme",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/SaifAbdelrazek011/saifurl/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/SaifAbdelrazek011/saifurl.git"
|
||||||
|
},
|
||||||
|
"license": "ISC",
|
||||||
|
"author": "",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"method-override": "^3.0.0",
|
||||||
|
"mongoose": "^8.14.0",
|
||||||
|
"nanoid": "^5.1.5",
|
||||||
|
"shortid": "^2.2.17"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.10"
|
||||||
|
}
|
||||||
|
}
|
147
server.js
Normal file
147
server.js
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import express from "express";
|
||||||
|
import mongoose from "mongoose";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import ShortUrl from "./modules/shortUrl.js";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
import methodOverride from "method-override";
|
||||||
|
|
||||||
|
dotenv.config({ path: ".env.local" });
|
||||||
|
|
||||||
|
const MONGODB_URI = process.env.MONGODB_URI;
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const accessToken = process.env.ACCESS_TOKEN;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(methodOverride("_method"));
|
||||||
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
|
||||||
|
// View engine and body parsing
|
||||||
|
app.set("view engine", "ejs");
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
mongoose
|
||||||
|
.connect(MONGODB_URI)
|
||||||
|
.then(() => console.log("Connected to MongoDB"))
|
||||||
|
.catch((err) => console.error("Failed to connect to MongoDB", err));
|
||||||
|
|
||||||
|
app.get("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const token = req.query.token;
|
||||||
|
const shortUrls = await ShortUrl.find();
|
||||||
|
|
||||||
|
if (token === accessToken) {
|
||||||
|
return res.render("admin", { shortUrls: shortUrls, token: token });
|
||||||
|
}
|
||||||
|
res.render("viewer", { shortUrls: shortUrls });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching short URLs:", error);
|
||||||
|
res.status(500).send("Internal Server Error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// CREATE A NEW SHORT URL
|
||||||
|
app.post("/shorturls", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { fullUrl, token, customShortUrl } = req.body;
|
||||||
|
const domain = `${req.protocol}://${req.headers.host}`;
|
||||||
|
|
||||||
|
if (token !== accessToken) {
|
||||||
|
return res.status(403).send("Forbidden: Invalid access token");
|
||||||
|
}
|
||||||
|
if (!fullUrl) {
|
||||||
|
return res.status(400).send("Full URL is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const short = customShortUrl || nanoid(7);
|
||||||
|
const existingShort = await ShortUrl.findOne({ short });
|
||||||
|
if (existingShort) {
|
||||||
|
return res.status(400).send("Custom short URL already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingFull = await ShortUrl.findOne({ full: fullUrl });
|
||||||
|
if (existingFull) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.send(
|
||||||
|
`Full URL already exists at <a href="${domain}/${existingFull.short}">${domain}/${existingFull.short}</a>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ShortUrl.create({ full: fullUrl, short });
|
||||||
|
res.redirect("/?token=" + token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating short URL:", error);
|
||||||
|
res.status(500).send("Internal Server Error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// REDIRECT TO THE FULL URL
|
||||||
|
app.get("/:shorturl", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const shortUrl = await ShortUrl.findOne({ short: req.params.shorturl });
|
||||||
|
if (!shortUrl) return res.sendStatus(404);
|
||||||
|
|
||||||
|
shortUrl.clicks++;
|
||||||
|
await shortUrl.save();
|
||||||
|
res.redirect(shortUrl.full);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching short URL:", error);
|
||||||
|
res.status(500).send("Internal Server Error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// UPDATE AN EXISTING SHORT URL
|
||||||
|
app.patch("/shorturls/:id", async (req, res) => {
|
||||||
|
const { fullUrl, customShortUrl } = req.body;
|
||||||
|
const token = req.query.token;
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
if (token !== accessToken) {
|
||||||
|
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await ShortUrl.findByIdAndUpdate(
|
||||||
|
id,
|
||||||
|
{ full: fullUrl, short: customShortUrl },
|
||||||
|
{ new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ success: false, message: "Short URL not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, updated });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ success: false, message: "Server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE A SHORT URL
|
||||||
|
app.delete("/shorturls/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { token } = req.query;
|
||||||
|
|
||||||
|
if (token !== accessToken) {
|
||||||
|
return res.status(403).send("Forbidden: Invalid access token");
|
||||||
|
}
|
||||||
|
|
||||||
|
await ShortUrl.findByIdAndDelete(id);
|
||||||
|
res.redirect("/?token=" + token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting short URL:", error);
|
||||||
|
res.status(500).send("Internal Server Error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// START THE SERVER
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on port ${PORT}`);
|
||||||
|
});
|
251
views/admin.ejs
Normal file
251
views/admin.ejs
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"
|
||||||
|
/>
|
||||||
|
<!-- Bootstrap JS Bundle (includes Popper) -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<title>SaifURL Admin</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container py-5">
|
||||||
|
<h1 class="text-center mb-4 fw-bold text-primary">Saif URLs</h1>
|
||||||
|
|
||||||
|
<!-- Token input and New URL Button -->
|
||||||
|
<form
|
||||||
|
action="/shorturls"
|
||||||
|
method="POST"
|
||||||
|
class="my-4 p-5 bg-white shadow-lg rounded"
|
||||||
|
>
|
||||||
|
<h2 class="text-center mb-4 fw-bold text-secondary">
|
||||||
|
Create a Short URL
|
||||||
|
</h2>
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Full URL -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="fullUrl" class="form-label fw-bold">Full URL</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
placeholder="Enter the full URL"
|
||||||
|
type="url"
|
||||||
|
name="fullUrl"
|
||||||
|
id="fullUrl"
|
||||||
|
class="form-control border-secondary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="token"
|
||||||
|
id="tokeninput"
|
||||||
|
value="<%= token %>"
|
||||||
|
/>
|
||||||
|
<!-- Custom Short URL -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="customShortUrl" class="form-label fw-bold"
|
||||||
|
>Custom Short URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="customShortUrl"
|
||||||
|
id="customShortUrl"
|
||||||
|
placeholder="Enter custom short URL (optional)"
|
||||||
|
class="form-control border-secondary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
|
<button class="btn btn-success w-100 fw-bold" type="submit">
|
||||||
|
Shorten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive shadow-sm rounded bg-white p-4">
|
||||||
|
<table class="table table-striped table-hover align-middle">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Full URL</th>
|
||||||
|
<th>Short</th>
|
||||||
|
<th>Clicks</th>
|
||||||
|
<th>Options</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% shortUrls.forEach(shortUrl => { %>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="<%= shortUrl.full %>"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none text-primary fw-bold"
|
||||||
|
><%= shortUrl.full %></a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="<%= shortUrl.short %>"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none text-success fw-bold"
|
||||||
|
><%= shortUrl.short %></a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td class="text-center fw-bold"><%= shortUrl.clicks %></td>
|
||||||
|
<td>
|
||||||
|
<!-- Edit Button -->
|
||||||
|
<button
|
||||||
|
class="btn btn-warning btn-sm me-2 fw-bold"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editModal<%= shortUrl._id %>"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<button
|
||||||
|
onclick="deleteUrl('<%= shortUrl._id %>')"
|
||||||
|
class="btn btn-danger btn-sm"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
id="editModal<%= shortUrl._id %>"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="editModalLabel<%= shortUrl._id %>"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="editForm<%= shortUrl._id %>">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5
|
||||||
|
class="modal-title"
|
||||||
|
id="editModalLabel<%= shortUrl._id %>"
|
||||||
|
>
|
||||||
|
Edit URL
|
||||||
|
</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label
|
||||||
|
for="fullUrl<%= shortUrl._id %>"
|
||||||
|
class="form-label"
|
||||||
|
>Full URL</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
class="form-control"
|
||||||
|
id="fullUrl<%= shortUrl._id %>"
|
||||||
|
name="fullUrl"
|
||||||
|
value="<%= shortUrl.full %>"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label
|
||||||
|
for="customShortUrl<%= shortUrl._id %>"
|
||||||
|
class="form-label"
|
||||||
|
>Custom Short URL</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="customShortUrl<%= shortUrl._id %>"
|
||||||
|
name="customShortUrl"
|
||||||
|
value="<%= shortUrl.short %>"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const form = document.getElementById("editForm<%= shortUrl._id %>");
|
||||||
|
const fullUrlInput = document.getElementById("fullUrl<%= shortUrl._id %>");
|
||||||
|
const customShortUrlInput = document.getElementById("customShortUrl<%= shortUrl._id %>");
|
||||||
|
const token = "<%= token %>";
|
||||||
|
const url = `/shorturls/<%= shortUrl._id %>?token=${token}`;
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const fullUrl = fullUrlInput.value;
|
||||||
|
const customShortUrl = customShortUrlInput.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ fullUrl, customShortUrl }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Server error while updating.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert("Failed to update URL: " + (data.message || "Unknown error"));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update error:", error);
|
||||||
|
alert("Something went wrong while updating the URL.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function deleteUrl(id) {
|
||||||
|
const token = new URLSearchParams(window.location.search).get("token");
|
||||||
|
if (!confirm("Delete this URL?")) return;
|
||||||
|
await fetch(`/shorturls/${id}?token=${token}`, { method: "DELETE" });
|
||||||
|
window.location.reload(); // Reload after deletion
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
68
views/viewer.ejs
Normal file
68
views/viewer.ejs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"
|
||||||
|
/>
|
||||||
|
<title>SaifURL Viewer</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container py-5">
|
||||||
|
<!-- Page Title -->
|
||||||
|
<h1 class="text-center mb-4 fw-bold text-primary">Saif URLs Viewer</h1>
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p class="mb-2 fw-bold">View the project on:</p>
|
||||||
|
<a
|
||||||
|
href="https://github.com/saifabdelrazek011/saifurl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="btn btn-outline-primary btn-sm me-2"
|
||||||
|
>
|
||||||
|
GitHub Repository
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Section -->
|
||||||
|
<div class="table-responsive shadow-sm rounded bg-white p-4 mt-2">
|
||||||
|
<table class="table table-striped table-hover align-middle">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Full URL</th>
|
||||||
|
<th>Short URL</th>
|
||||||
|
<th>Clicks</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% shortUrls.forEach(shortUrl => { %>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="<%= shortUrl.full %>"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none text-primary fw-bold"
|
||||||
|
><%= shortUrl.full %></a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="<%= shortUrl.short %>"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none text-success fw-bold"
|
||||||
|
><%= shortUrl.short %></a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td class="text-center fw-bold"><%= shortUrl.clicks %></td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS Bundle -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue