This commit is contained in:
Elijah McMorris 2024-02-19 08:58:07 -08:00
parent b0f3e6c576
commit e44d419b58
Signed by: NexVeridian
SSH key fingerprint: SHA256:bsA1SKZxuEcEVHAy3gY1HUeM5ykRJl0U0kQHQn0hMg8
15 changed files with 233 additions and 82 deletions

View file

@ -1,20 +1,37 @@
# next-url-shortener
A simple URL shortener using [Next.js](https://nextjs.org/) 14 server actions, [Postgres](https://www.postgresql.org/) or [SurrealDB](https://surrealdb.com/), [Shadcn/ui](http://ui.shadcn.com/) and [Tailwind](https://tailwindcss.com/).
# Install
Copy [docker-compose.yml](./docker-compose.yml)
### Copy one
- [docker-compose-postgres.yml](./docker-compose-postgres.yml)
- [docker-compose-surrealdb.yml](./docker-compose-surrealdb.yml)
Create data folder next to docker-compose.yml and .env, and set the data type in .env
```
├── data
│ └── surrealdb
├── docker-compose.yml
├── docker-compose-postgres.yml
└── .env
```
`docker compose up --pull always -d`
### Then run:
- `docker compose -f docker-compose-postgres.yml up --pull always -d`
- `docker compose -f docker-compose-surrealdb.yml up --pull always -d`
## Example .env
```
# If not using docker, use 0.0.0.0:8000
DB_URL_PORT=surrealdb:8000
# postgres or surrealdb
DB_TYPE=postgres
# For surrealdb: If using docker surrealdb:8000, if not use 0.0.0.0:8000
# For postgres: If using docker postgres:5432, if not use 0.0.0.0:5432
DB_URL_PORT=postgres:5432
# postgres
POSTGRES_USER=root
POSTGRES_PASSWORD=root
POSTGRES_DB=url
# surrealdb
DB_USER=root
DB_PASSWORD=root
```

View file

@ -0,0 +1,36 @@
version: "3"
services:
next-url-shortener:
container_name: next-url-shortener
build: .
env_file:
- .env
ports:
- 3000:3000
depends_on:
- postgres
networks:
- postgres
postgres:
container_name: postgres
image: postgres:latest
env_file:
- .env
restart: always
deploy:
resources:
reservations:
cpus: 1
ports:
- 5432:5432
volumes:
- ./data/postgres:/var/lib/postgresql/data
networks:
- postgres
volumes:
data:
networks:
postgres:

17
package-lock.json generated
View file

@ -22,8 +22,9 @@
"dotenv": "^16.4.4",
"next": "14.1.0",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
"postgres": "^3.4.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.50.1",
"surrealdb.js": "^0.11.0",
"swr": "^2.2.5",
@ -4465,6 +4466,18 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/postgres": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.3.tgz",
"integrity": "sha512-iHJn4+M9vbTdHSdDzNkC0crHq+1CUdFhx+YqCE+SqWxPjm+Zu63jq7yZborOBF64c8pc58O5uMudyL1FQcHacA==",
"engines": {
"node": ">=12"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/porsager"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View file

@ -23,8 +23,8 @@
"dotenv": "^16.4.4",
"next": "14.1.0",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
"postgres": "^3.4.3",
"react": "^18.2.0",
"react-hook-form": "^7.50.1",
"surrealdb.js": "^0.11.0",
"swr": "^2.2.5",

View file

@ -1,16 +1,35 @@
"use server";
import { initConnection } from "@/components/db-utils";
import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils";
export async function querydb(slug: string) {
let long_url = undefined;
try {
let db = await initConnection();
if (process.env.DB_TYPE === "surrealdb") {
let db = await initConnectionSurreal();
let long_url = await db.query(`
update url:[$id] set clicks = clicks + 1 ;
update url:[$id]
set clicks = clicks + 1;
select * from url:[$id];
`, { id: slug });
// @ts-ignore
long_url = long_url[0][0].long_url;
}
if (process.env.DB_TYPE === "postgres") {
let sql = await initConnectionPostgres();
long_url = await sql`
update url set
clicks = clicks + 1,
date_accessed = now()
where id = ${slug}
returning long_url;
`;
long_url = long_url[0].long_url;
}
console.log(long_url);
return long_url;
} catch (e) {
return;

View file

@ -1,27 +1,70 @@
"use server";
import { initConnection } from "@/components/db-utils";
import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils";
import { formSchema } from "./schema";
function generateRandomString(length: number) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomString = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
randomString += charset[randomIndex];
}
return randomString;
}
export async function querydb(prevState: any, formData: FormData) {
try {
const values = formSchema.safeParse(formData)
if (!values.success) {
return { error: values.error };
}
const long_url = values.data.url;
let url = undefined;
let db = await initConnection();
let url = await db.query(`
try {
if (process.env.DB_TYPE === "surrealdb") {
let db = await initConnectionSurreal();
url = await db.query(`
create url:[rand::string(8)] CONTENT {
long_url: string::replace(string::replace($long_url, "https://", ""), "http://", ""),
long_url: $long_url,
clicks: 0,
date_added: time::now(),
date_accessed: <future> { time::now() }
} return id[0];
`, { long_url: long_url });
`, {
long_url: long_url.replace("https://", "").replace("http://", "")
});
// @ts-ignore
url = url[0][0].id;
}
console.log(long_url);
if (process.env.DB_TYPE === "postgres") {
let sql = await initConnectionPostgres();
await sql`
create table if not exists url (
id text primary key,
clicks integer not null,
long_url text not null,
date_added timestamp not null,
date_accessed timestamp not null
);
`;
url = await sql`
insert into url (id, long_url, clicks, date_added, date_accessed)
values (
${generateRandomString(8)},
${long_url.replace("https://", "").replace("http://", "")},
0,
now(),
now()
)
returning id;
`;
url = url[0].id;
}
console.log(url);
return { url: url };
} catch (e) {

View file

@ -6,12 +6,16 @@ import {
CardTitle
} from "@/components/ui/card";
export default function GlobalError() {
export default function GlobalError({
error,
}: {
error: Error & { digest?: string }
}) {
return (
<CardGrid max_rows={1}>
<CardGrid maxCols={1}>
<Card>
<CardHeader>
<CardTitle className="text-center text-2xl text-red-400">Error</CardTitle>
<CardTitle className="text-center text-2xl text-red-400">Error + {String(error)}</CardTitle>
</CardHeader>
</Card>
</CardGrid>

View file

@ -8,7 +8,7 @@ import {
export default function NotFound() {
return (
<CardGrid max_rows={1}>
<CardGrid maxCols={1}>
<Card>
<CardHeader>
<CardTitle className="text-center text-2xl text-red-400">404 - Not Found</CardTitle>

View file

@ -1,11 +1,13 @@
"use server";
import { initConnection } from "@/components/db-utils";
import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils";
export async function querydb() {
try {
let db = await initConnection();
let stats = [];
if (process.env.DB_TYPE === "surrealdb") {
let db = await initConnectionSurreal();
// console.log(db);
let stats = await db.query(`
stats = await db.query(`
select * from url
order by clicks desc
limit 50;
@ -13,6 +15,16 @@ export async function querydb() {
// @ts-ignore
stats = stats[0];
}
if (process.env.DB_TYPE === "postgres") {
let sql = await initConnectionPostgres();
stats = await sql`
select * from url
order by clicks desc
limit 50;
`;
}
return stats;
} catch (e) {

View file

@ -8,7 +8,7 @@ import {
export default function Loading() {
return (
<CardGrid max_rows={1}>
<CardGrid maxCols={1}>
<Card>
<CardHeader>
<CardTitle className="text-1xl text-amber-400">Loading...</CardTitle>

View file

@ -44,7 +44,7 @@ export default function StatsPage() {
return data.length === 0 ? (
<Loading />
) : (
<CardGrid max_rows={1}>
<CardGrid maxCols={1}>
<Card>
{/* @ts-ignore */}
<DataTable columns={columns} data={data} />

View file

@ -1,37 +1,30 @@
"use client";
export default function CardGrid({
max_rows = 4,
maxCols: maxCols = 4,
children,
className,
...props
}: {
max_rows?: number;
maxCols?: number;
children?: React.ReactNode;
className?: string;
}) {
let baseClassName = `hidden items-start justify-center gap-6 rounded-lg p-8 md:grid`;
let baseClassName = `hidden items-start justify-center gap-6 rounded-lg p-8 md:grid md:grid-cols-1`;
switch (max_rows) {
case 1:
baseClassName += " md:grid-cols-1 ";
break;
case 2:
baseClassName += " md:grid-cols-1 lg:grid-cols-2 ";
break;
case 3:
baseClassName += " md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 ";
break;
case 4:
baseClassName += " md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 ";
break;
default:
break;
};
if (maxCols >= 2) {
baseClassName += " lg:grid-cols-2";
}
if (maxCols >= 3) {
baseClassName += " xl:grid-cols-3";
}
if (maxCols >= 4) {
baseClassName += " 2xl:grid-cols-4";
}
if (className == undefined) {
className = baseClassName;
} else {
className = baseClassName + className;
className = baseClassName + " " + className;
}
return (
<div

View file

@ -1,9 +1,9 @@
"use server";
import "dotenv";
import postgres, { Sql } from 'postgres';
import { Surreal } from "surrealdb.js";
const db = new Surreal();
export async function initConnection(): Promise<Surreal> {
export async function initConnectionSurreal(): Promise<Surreal> {
try {
db.connect("ws://" + process.env.DB_URL_PORT + "/rpc", {
namespace: "url",
@ -19,3 +19,17 @@ export async function initConnection(): Promise<Surreal> {
return db;
}
export async function initConnectionPostgres(): Promise<Sql<{}>> {
const DB_URL_PORT = (process.env.DB_URL_PORT || "postgres:5432").split(":");
const sql = postgres({
host: DB_URL_PORT[0],
port: parseInt(DB_URL_PORT[1]),
user: process.env.POSTGRES_USER || "root",
password: process.env.POSTGRES_PASSWORD || "root",
database: process.env.POSTGRES_DB || "url",
max: 100,
onnotice: () => { },
});
return sql;
}