chore: format
This commit is contained in:
parent
c8338e5516
commit
527139f298
33 changed files with 848 additions and 3132 deletions
|
@ -1,16 +0,0 @@
|
||||||
# https://mcr.microsoft.com/en-us/product/devcontainers/typescript-node/about
|
|
||||||
FROM mcr.microsoft.com/devcontainers/typescript-node:20-bookworm
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install build-essential xz-utils
|
|
||||||
|
|
||||||
# RUN LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep -Po '"tag_name": "v\K[0-9.]+') && \
|
|
||||||
# curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" && \
|
|
||||||
# sudo tar xf lazygit.tar.gz -C /usr/local/bin lazygit && \
|
|
||||||
# rm -rf lazygit.tar.gz
|
|
||||||
|
|
||||||
# RUN BTOP_VERSION=$(curl -s "https://api.github.com/repos/aristocratos/btop/releases/latest" | grep -Po '"tag_name": "v\K[0-9.]+') && \
|
|
||||||
# wget "https://github.com/aristocratos/btop/releases/download/v${BTOP_VERSION}/btop-x86_64-linux-musl.tbz" && \
|
|
||||||
# sudo tar -xvf btop-x86_64-linux-musl.tbz && \
|
|
||||||
# cd btop && ./install.sh && cd .. && \
|
|
||||||
# rm -rf btop-x86_64-linux-musl.tbz btop
|
|
|
@ -1,60 +0,0 @@
|
||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
|
||||||
{
|
|
||||||
"name": "Node.js & TypeScript",
|
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
|
||||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-20",
|
|
||||||
"build": {
|
|
||||||
// Path is relataive to the devcontainer.json file.
|
|
||||||
"dockerfile": "Dockerfile"
|
|
||||||
},
|
|
||||||
// https://github.com/microsoft/vscode-remote-release/issues/2485#issuecomment-1156342780
|
|
||||||
"runArgs": [
|
|
||||||
"--name",
|
|
||||||
"devcontainer-${containerWorkspaceFolderBasename}"
|
|
||||||
],
|
|
||||||
"initializeCommand": "docker rm -f devcontainer-${containerWorkspaceFolderBasename} || true",
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/git:1": {},
|
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
|
||||||
"ghcr.io/devcontainers/features/nix:1": {
|
|
||||||
"packages": [
|
|
||||||
"btop",
|
|
||||||
"lazygit",
|
|
||||||
"nixpkgs-fmt"
|
|
||||||
],
|
|
||||||
"extraNixConfig": "experimental-features = nix-command flakes"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// "forwardPorts": [],
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
// "postCreateCommand": "yarn install",
|
|
||||||
"postAttachCommand": {
|
|
||||||
"AddGitSafeDir": "git config --global --add safe.directory /workspaces/${containerWorkspaceFolderBasename}"
|
|
||||||
},
|
|
||||||
"onCreateCommand": {
|
|
||||||
"nix-shell": "nix-shell --command 'echo done install packages'"
|
|
||||||
},
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
// "customizations": {},
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
"remoteUser": "root",
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
|
||||||
"mutantdino.resourcemonitor",
|
|
||||||
"christian-kohler.path-intellisense",
|
|
||||||
"Gruntfuggly.todo-tree",
|
|
||||||
"ms-azuretools.vscode-docker",
|
|
||||||
"redhat.vscode-yaml",
|
|
||||||
"bradlc.vscode-tailwindcss",
|
|
||||||
"ms-vscode.vscode-typescript-next",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"GitHub.copilot",
|
|
||||||
"GitHub.copilot-chat"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "next/core-web-vitals"
|
|
||||||
}
|
|
36
biome.json
Normal file
36
biome.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": false,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": false
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false,
|
||||||
|
"ignore": [".next", "./src/components/ui"]
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "tab"
|
||||||
|
},
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"complexity": { "noForEach": "off", "noBannedTypes": "off" },
|
||||||
|
"suspicious": {
|
||||||
|
"noShadowRestrictedNames": "off",
|
||||||
|
"noExplicitAny": "off",
|
||||||
|
"noArrayIndexKey": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,16 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "new-york",
|
||||||
"rsc": false,
|
"rsc": false,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.ts",
|
"config": "tailwind.config.ts",
|
||||||
"css": "src/app/globals.css",
|
"css": "src/app/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true
|
"cssVariables": true
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils"
|
"utils": "@/lib/utils"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig;
|
||||||
|
|
96
package.json
96
package.json
|
@ -1,50 +1,48 @@
|
||||||
{
|
{
|
||||||
"name": "next-url-shortener",
|
"name": "next-url-shortener",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"fix": "biome check . --write",
|
||||||
},
|
"check": "biome check ."
|
||||||
"dependencies": {
|
},
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"dependencies": {
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
"@radix-ui/react-popover": "^1.1.6",
|
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||||
"@radix-ui/react-select": "^2.1.6",
|
"@radix-ui/react-popover": "^1.1.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"clsx": "^2.1.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"dotenv": "^16.4.7",
|
"clsx": "^2.1.1",
|
||||||
"next": "14.2.5",
|
"dotenv": "^16.4.7",
|
||||||
"next-themes": "^0.3.0",
|
"next": "14.2.5",
|
||||||
"postgres": "^3.4.5",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
"postgres": "^3.4.5",
|
||||||
"react-hook-form": "^7.54.2",
|
"react": "^18.3.1",
|
||||||
"surrealdb.js": "^1.0.0",
|
"react-hook-form": "^7.54.2",
|
||||||
"swr": "^2.3.2",
|
"surrealdb.js": "^1.0.0",
|
||||||
"tailwind-merge": "^2.6.0",
|
"swr": "^2.3.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwind-merge": "^2.6.0",
|
||||||
"ws": "^8.18.1",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.24.2"
|
"ws": "^8.18.1",
|
||||||
},
|
"zod": "^3.24.2"
|
||||||
"devDependencies": {
|
},
|
||||||
"@types/node": "^22.13.9",
|
"devDependencies": {
|
||||||
"@types/prop-types": "^15.7.14",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/react": "^18.3.18",
|
"@types/node": "^22.13.9",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/prop-types": "^15.7.14",
|
||||||
"autoprefixer": "^10.4.20",
|
"@types/react": "^18.3.18",
|
||||||
"eslint": "^8.57.1",
|
"@types/react-dom": "^18.3.5",
|
||||||
"eslint-config-next": "14.2.5",
|
"autoprefixer": "^10.4.20",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"tailwindcss": "^3.4.17",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"typescript": "^5.8.2"
|
||||||
"tailwindcss": "^3.4.17",
|
}
|
||||||
"typescript": "^5.8.2"
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
2401
pnpm-lock.yaml
generated
2401
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
{ pkgs ? import <nixpkgs> {} }:
|
|
||||||
pkgs.mkShell {
|
|
||||||
packages = with pkgs; [
|
|
||||||
git
|
|
||||||
btop
|
|
||||||
lazygit
|
|
||||||
micro
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,34 +1,40 @@
|
||||||
"use server";
|
"use server";
|
||||||
import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils";
|
import {
|
||||||
|
initConnectionPostgres,
|
||||||
|
initConnectionSurreal,
|
||||||
|
} from "@/components/db-utils";
|
||||||
|
|
||||||
export async function querydb(slug: string) {
|
export async function querydb(slug: string) {
|
||||||
let long_url = undefined;
|
let long_url = undefined;
|
||||||
try {
|
try {
|
||||||
if (process.env.DB_TYPE === "surrealdb") {
|
if (process.env.DB_TYPE === "surrealdb") {
|
||||||
let db = await initConnectionSurreal();
|
const db = await initConnectionSurreal();
|
||||||
long_url = await db.query(`
|
long_url = await db.query(
|
||||||
|
`
|
||||||
update url:[$id]
|
update url:[$id]
|
||||||
set clicks +=1;
|
set clicks +=1;
|
||||||
`, { id: slug });
|
`,
|
||||||
// @ts-ignore
|
{ id: slug },
|
||||||
long_url = long_url[0][0].long_url;
|
);
|
||||||
}
|
// @ts-ignore
|
||||||
|
long_url = long_url[0][0].long_url;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.DB_TYPE === "postgres") {
|
if (process.env.DB_TYPE === "postgres") {
|
||||||
let sql = await initConnectionPostgres();
|
const sql = await initConnectionPostgres();
|
||||||
long_url = await sql`
|
long_url = await sql`
|
||||||
update url set
|
update url set
|
||||||
clicks = clicks + 1,
|
clicks = clicks + 1,
|
||||||
date_accessed = now()
|
date_accessed = now()
|
||||||
where id = ${slug}
|
where id = ${slug}
|
||||||
returning long_url;
|
returning long_url;
|
||||||
`;
|
`;
|
||||||
long_url = long_url[0].long_url;
|
long_url = long_url[0].long_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(long_url);
|
console.log(long_url);
|
||||||
return long_url;
|
return long_url;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { notFound, redirect } from "next/navigation";
|
||||||
import { querydb } from "./db";
|
import { querydb } from "./db";
|
||||||
|
|
||||||
export default async function Page({ params }: { params: { slug: string } }) {
|
export default async function Page({ params }: { params: { slug: string } }) {
|
||||||
if (params.slug == "favicon.ico") {
|
if (params.slug === "favicon.ico") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let long_url = await querydb(params.slug);
|
const long_url = await querydb(params.slug);
|
||||||
if (long_url == undefined) {
|
if (long_url === undefined) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
redirect(`https://${long_url}`);
|
redirect(`https://${long_url}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,117 +1,116 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CopyIcon } from "@radix-ui/react-icons";
|
import { CopyIcon } from "@radix-ui/react-icons";
|
||||||
import { useFormState } from "react-dom";
|
import { useFormState } from "react-dom";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { querydb } from "./db";
|
import { querydb } from "./db";
|
||||||
import { formSchema } from "./schema";
|
import { formSchema } from "./schema";
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
url: null,
|
url: null,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function CreateCard() {
|
export default function CreateCard() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let [state, formAction] = useFormState(querydb, initialState);
|
const [state, formAction] = useFormState(querydb, initialState);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
url: "",
|
url: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCopyUrl = () => {
|
const handleCopyUrl = () => {
|
||||||
if (state && state.url) {
|
if (state && state.url) {
|
||||||
let url = undefined;
|
let url = undefined;
|
||||||
const currentSiteName = window.location.hostname;
|
const currentSiteName = window.location.hostname;
|
||||||
|
|
||||||
if (state.url.includes("https://")) {
|
if (state.url.includes("https://")) {
|
||||||
url = state.url;
|
url = state.url;
|
||||||
} else {
|
} else {
|
||||||
if (currentSiteName === "localhost" || currentSiteName === "0.0.0.0") {
|
if (currentSiteName === "localhost" || currentSiteName === "0.0.0.0") {
|
||||||
url = `http://${currentSiteName}:${window.location.port}/${state.url.toString()}`;
|
url = `http://${currentSiteName}:${window.location.port}/${state.url.toString()}`;
|
||||||
} else {
|
} else {
|
||||||
url = `https://${currentSiteName}/${state.url.toString()}`;
|
url = `https://${currentSiteName}/${state.url.toString()}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.clipboard.writeText(url)
|
navigator.clipboard.writeText(url).catch((err) => {
|
||||||
.catch(err => {
|
console.error("Failed to copy URL to clipboard:", err);
|
||||||
console.error('Failed to copy URL to clipboard:', err);
|
});
|
||||||
});
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <CardGrid>
|
// <CardGrid>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Create a Shortened URL</CardTitle>
|
<CardTitle>Create a Shortened URL</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<form id="url_form" action={form.handleSubmit(formAction)} className="space-y-8">
|
<form id="url_form" action={form.handleSubmit(formAction)} className="space-y-8">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="url"
|
name="url"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Enter a url</FormLabel>
|
<FormLabel>Enter a url</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="nexveridian.com" {...field} />
|
<Input placeholder="nexveridian.com" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="flex flex-row gap-4">
|
<CardFooter className="flex flex-row gap-4">
|
||||||
<Button type="submit" form="url_form">Submit</Button>
|
<Button type="submit" form="url_form">
|
||||||
{state && state.url && (
|
Submit
|
||||||
<Popover>
|
</Button>
|
||||||
<PopoverTrigger>
|
{state && state.url && (
|
||||||
<Button onClick={handleCopyUrl}>
|
<Popover>
|
||||||
<CopyIcon className="mr-2 h-4 w-4" /> Copy Url
|
<PopoverTrigger>
|
||||||
</Button>
|
<Button onClick={handleCopyUrl}>
|
||||||
</PopoverTrigger>
|
<CopyIcon className="mr-2 h-4 w-4" /> Copy Url
|
||||||
<PopoverContent>
|
</Button>
|
||||||
<p className="text-lg">
|
</PopoverTrigger>
|
||||||
Url added to clipboard
|
<PopoverContent>
|
||||||
</p>
|
<p className="text-lg">Url added to clipboard</p>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
// </CardGrid>
|
// </CardGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,54 @@
|
||||||
"use server";
|
"use server";
|
||||||
import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils";
|
import {
|
||||||
|
initConnectionPostgres,
|
||||||
|
initConnectionSurreal,
|
||||||
|
} from "@/components/db-utils";
|
||||||
import { formSchema } from "./schema";
|
import { formSchema } from "./schema";
|
||||||
|
|
||||||
function generateRandomString(length: number) {
|
function generateRandomString(length: number) {
|
||||||
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
const charset =
|
||||||
let randomString = '';
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
for (let i = 0; i < length; i++) {
|
let randomString = "";
|
||||||
const randomIndex = Math.floor(Math.random() * charset.length);
|
for (let i = 0; i < length; i++) {
|
||||||
randomString += charset[randomIndex];
|
const randomIndex = Math.floor(Math.random() * charset.length);
|
||||||
}
|
randomString += charset[randomIndex];
|
||||||
return randomString;
|
}
|
||||||
|
return randomString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function querydb(prevState: any, formData: FormData) {
|
export async function querydb(prevState: any, formData: FormData) {
|
||||||
const values = formSchema.safeParse(formData)
|
const values = formSchema.safeParse(formData);
|
||||||
if (!values.success) {
|
if (!values.success) {
|
||||||
return { error: values.error };
|
return { error: values.error };
|
||||||
}
|
}
|
||||||
let long_url = values.data.url.replace("https://", "").replace("http://", "");
|
let long_url = values.data.url.replace("https://", "").replace("http://", "");
|
||||||
long_url = long_url.endsWith('/') ? long_url.slice(0, -1) : long_url;
|
long_url = long_url.endsWith("/") ? long_url.slice(0, -1) : long_url;
|
||||||
let url = undefined;
|
let url = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.env.DB_TYPE === "surrealdb") {
|
if (process.env.DB_TYPE === "surrealdb") {
|
||||||
let db = await initConnectionSurreal();
|
const db = await initConnectionSurreal();
|
||||||
url = await db.query(`
|
url = await db.query(
|
||||||
|
`
|
||||||
create url:[rand::string(8)] CONTENT {
|
create url:[rand::string(8)] CONTENT {
|
||||||
long_url: $long_url,
|
long_url: $long_url,
|
||||||
clicks: 0,
|
clicks: 0,
|
||||||
date_added: time::now(),
|
date_added: time::now(),
|
||||||
date_accessed: <future> { time::now() }
|
date_accessed: <future> { time::now() }
|
||||||
} return id[0];
|
} return id[0];
|
||||||
`, {
|
`,
|
||||||
long_url: long_url
|
{
|
||||||
});
|
long_url: long_url,
|
||||||
// @ts-ignore
|
},
|
||||||
url = url[0][0].id;
|
);
|
||||||
}
|
// @ts-ignore
|
||||||
|
url = url[0][0].id;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(long_url);
|
console.log(long_url);
|
||||||
if (process.env.DB_TYPE === "postgres") {
|
if (process.env.DB_TYPE === "postgres") {
|
||||||
let sql = await initConnectionPostgres();
|
const sql = await initConnectionPostgres();
|
||||||
await sql`
|
await sql`
|
||||||
create table if not exists url (
|
create table if not exists url (
|
||||||
id text primary key,
|
id text primary key,
|
||||||
clicks integer not null,
|
clicks integer not null,
|
||||||
|
@ -51,7 +58,7 @@ export async function querydb(prevState: any, formData: FormData) {
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
url = await sql`
|
url = await sql`
|
||||||
insert into url (id, long_url, clicks, date_added, date_accessed)
|
insert into url (id, long_url, clicks, date_added, date_accessed)
|
||||||
values (
|
values (
|
||||||
${generateRandomString(8)},
|
${generateRandomString(8)},
|
||||||
|
@ -63,17 +70,17 @@ export async function querydb(prevState: any, formData: FormData) {
|
||||||
returning id;
|
returning id;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
url = url[0].id;
|
url = url[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(url);
|
console.log(url);
|
||||||
|
|
||||||
if (process.env.OVERRIDE_URL !== undefined) {
|
if (process.env.OVERRIDE_URL !== undefined) {
|
||||||
url = `https://${process.env.OVERRIDE_URL}/${url.toString()}`;
|
url = `https://${process.env.OVERRIDE_URL}/${url.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { url: url };
|
return { url: url };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
url: z.string().min(4,
|
url: z
|
||||||
{ message: "The URL must be at least 4 characters long" }
|
.string()
|
||||||
).max(100
|
.min(4, { message: "The URL must be at least 4 characters long" })
|
||||||
, { message: "The URL must be at most 100 characters long" }),
|
.max(100, { message: "The URL must be at most 100 characters long" }),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
"use client";
|
"use client";
|
||||||
import CardGrid from "@/components/card-grid";
|
import CardGrid from "@/components/card-grid";
|
||||||
import {
|
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
|
|
||||||
export default function GlobalError({
|
export default function GlobalError({
|
||||||
error,
|
error,
|
||||||
}: {
|
}: {
|
||||||
error: Error & { digest?: string }
|
error: Error & { digest?: string };
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<CardGrid maxCols={1}>
|
<CardGrid maxCols={1}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center text-2xl text-red-400">Error + {String(error)}</CardTitle>
|
<CardTitle className="text-center text-2xl text-red-400">
|
||||||
</CardHeader>
|
Error + {String(error)}
|
||||||
</Card>
|
</CardTitle>
|
||||||
</CardGrid>
|
</CardHeader>
|
||||||
);
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,57 +3,57 @@
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 0 0% 3.9%;
|
--foreground: 0 0% 3.9%;
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 0 0% 3.9%;
|
--card-foreground: 0 0% 3.9%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 0 0% 3.9%;
|
--popover-foreground: 0 0% 3.9%;
|
||||||
--primary: 0 0% 9%;
|
--primary: 0 0% 9%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
--secondary: 0 0% 96.1%;
|
--secondary: 0 0% 96.1%;
|
||||||
--secondary-foreground: 0 0% 9%;
|
--secondary-foreground: 0 0% 9%;
|
||||||
--muted: 0 0% 96.1%;
|
--muted: 0 0% 96.1%;
|
||||||
--muted-foreground: 0 0% 45.1%;
|
--muted-foreground: 0 0% 45.1%;
|
||||||
--accent: 0 0% 96.1%;
|
--accent: 0 0% 96.1%;
|
||||||
--accent-foreground: 0 0% 9%;
|
--accent-foreground: 0 0% 9%;
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 0 0% 89.8%;
|
--border: 0 0% 89.8%;
|
||||||
--input: 0 0% 89.8%;
|
--input: 0 0% 89.8%;
|
||||||
--ring: 0 0% 3.9%;
|
--ring: 0 0% 3.9%;
|
||||||
--radius: 0.3rem;
|
--radius: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 0 0% 3.9%;
|
--background: 0 0% 3.9%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 0 0% 3.9%;
|
--card: 0 0% 3.9%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
--popover: 0 0% 3.9%;
|
--popover: 0 0% 3.9%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--primary: 0 0% 98%;
|
--primary: 0 0% 98%;
|
||||||
--primary-foreground: 0 0% 9%;
|
--primary-foreground: 0 0% 9%;
|
||||||
--secondary: 0 0% 14.9%;
|
--secondary: 0 0% 14.9%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--muted: 0 0% 14.9%;
|
--muted: 0 0% 14.9%;
|
||||||
--muted-foreground: 0 0% 63.9%;
|
--muted-foreground: 0 0% 63.9%;
|
||||||
--accent: 0 0% 14.9%;
|
--accent: 0 0% 14.9%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 0 0% 14.9%;
|
--border: 0 0% 14.9%;
|
||||||
--input: 0 0% 14.9%;
|
--input: 0 0% 14.9%;
|
||||||
--ring: 0 0% 83.1%;
|
--ring: 0 0% 83.1%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,33 +5,33 @@ import { Roboto_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
export const roboto_mono = Roboto_Mono({
|
export const roboto_mono = Roboto_Mono({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
display: "swap",
|
display: "swap",
|
||||||
variable: "--font-roboto-mono",
|
variable: "--font-roboto-mono",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Next Url Shortener",
|
title: "Next Url Shortener",
|
||||||
description: "Next Url Shortener",
|
description: "Next Url Shortener",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={roboto_mono.className}>
|
<body className={roboto_mono.className}>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="dark"
|
defaultTheme="dark"
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<Nav />
|
<Nav />
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
"use client";
|
"use client";
|
||||||
import CardGrid from "@/components/card-grid";
|
import CardGrid from "@/components/card-grid";
|
||||||
import {
|
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<CardGrid maxCols={1}>
|
<CardGrid maxCols={1}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center text-2xl text-red-400">404 - Not Found</CardTitle>
|
<CardTitle className="text-center text-2xl text-red-400">
|
||||||
</CardHeader>
|
404 - Not Found
|
||||||
</Card>
|
</CardTitle>
|
||||||
</CardGrid>
|
</CardHeader>
|
||||||
);
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,28 @@
|
||||||
"use client";
|
"use client";
|
||||||
import CardGrid from "@/components/card-grid";
|
import CardGrid from "@/components/card-grid";
|
||||||
import {
|
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import CreateCard from "./create/create";
|
import CreateCard from "./create/create";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<CardGrid>
|
<CardGrid>
|
||||||
{/* <Card>
|
{/* <Card>
|
||||||
<Link href="/create">
|
<Link href="/create">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Create a Shortened URL</CardTitle>
|
<CardTitle>Create a Shortened URL</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Link>
|
</Link>
|
||||||
</Card> */}
|
</Card> */}
|
||||||
<CreateCard />
|
<CreateCard />
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Link href="/stats">
|
<Link href="/stats">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Site Stats</CardTitle>
|
<CardTitle>Site Stats</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Link>
|
</Link>
|
||||||
</Card>
|
</Card>
|
||||||
</CardGrid>
|
</CardGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
|
||||||
export type UrlTable = {
|
export type UrlTable = {
|
||||||
clicks: number;
|
clicks: number;
|
||||||
date_accessed: Date;
|
date_accessed: Date;
|
||||||
date_added: Date;
|
date_added: Date;
|
||||||
id: string;
|
id: string;
|
||||||
long_url: string;
|
long_url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const columns: ColumnDef<UrlTable>[] = [
|
export const columns: ColumnDef<UrlTable>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "clicks",
|
accessorKey: "clicks",
|
||||||
header: "Clicks",
|
header: "Clicks",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "long_url",
|
accessorKey: "long_url",
|
||||||
header: "URL",
|
header: "URL",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
header: "Short URL",
|
header: "Short URL",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "date_accessed",
|
accessorKey: "date_accessed",
|
||||||
header: "Date Accessed",
|
header: "Date Accessed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "date_added",
|
accessorKey: "date_added",
|
||||||
header: "Date Added",
|
header: "Date Added",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,79 +1,79 @@
|
||||||
"use client";
|
"use client";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
type ColumnDef,
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
slug: string[];
|
slug: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
return (
|
return (
|
||||||
<TableHead key={header.id}>
|
<TableHead key={header.id}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext()
|
header.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{table.getRowModel().rows?.length ? (
|
{table.getRowModel().rows?.length ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={row.id}
|
key={row.id}
|
||||||
data-state={row.getIsSelected() && "selected"}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell key={cell.id}>
|
<TableCell key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
No results.
|
No results.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,36 @@
|
||||||
"use server";
|
"use server";
|
||||||
import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils";
|
import {
|
||||||
|
initConnectionPostgres,
|
||||||
|
initConnectionSurreal,
|
||||||
|
} from "@/components/db-utils";
|
||||||
|
|
||||||
export async function querydb() {
|
export async function querydb() {
|
||||||
try {
|
try {
|
||||||
let stats = [];
|
let stats = [];
|
||||||
if (process.env.DB_TYPE === "surrealdb") {
|
if (process.env.DB_TYPE === "surrealdb") {
|
||||||
let db = await initConnectionSurreal();
|
const db = await initConnectionSurreal();
|
||||||
// console.log(db);
|
// console.log(db);
|
||||||
stats = await db.query(`
|
stats = await db.query(`
|
||||||
select * from url
|
select * from url
|
||||||
order by clicks desc
|
order by clicks desc
|
||||||
limit 10;
|
limit 10;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
stats = stats[0];
|
stats = stats[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.DB_TYPE === "postgres") {
|
if (process.env.DB_TYPE === "postgres") {
|
||||||
let sql = await initConnectionPostgres();
|
const sql = await initConnectionPostgres();
|
||||||
stats = await sql`
|
stats = await sql`
|
||||||
select * from url
|
select * from url
|
||||||
order by clicks desc
|
order by clicks desc
|
||||||
limit 10;
|
limit 10;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
import CardGrid from "@/components/card-grid";
|
import CardGrid from "@/components/card-grid";
|
||||||
import {
|
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
<CardGrid maxCols={1}>
|
<CardGrid maxCols={1}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-1xl text-amber-400">Loading...</CardTitle>
|
<CardTitle className="text-1xl text-amber-400">Loading...</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
</CardGrid>
|
</CardGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,47 +8,47 @@ import { querydb } from "./db";
|
||||||
import Loading from "./loading";
|
import Loading from "./loading";
|
||||||
|
|
||||||
export default function StatsPage() {
|
export default function StatsPage() {
|
||||||
let [data, setData] = useState([]);
|
let [data, setData] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const result = await querydb();
|
const result = await querydb();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
setData(result);
|
setData(result);
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (data.length !== 0 && data !== undefined && data !== null) {
|
if (data.length !== 0 && data !== undefined && data !== null) {
|
||||||
const formatDate = (dateString: string | number | Date) => {
|
const formatDate = (dateString: string | number | Date) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
const year = String(date.getFullYear()).slice(-2);
|
const year = String(date.getFullYear()).slice(-2);
|
||||||
return `${month}/${day}/${year}`;
|
return `${month}/${day}/${year}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data = data.map(item => ({
|
data = data.map((item) => ({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
...item,
|
...item,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
date_accessed: formatDate(item.date_accessed),
|
date_accessed: formatDate(item.date_accessed),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
date_added: formatDate(item.date_added),
|
date_added: formatDate(item.date_added),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
id: item.id.replace(/^url:\['(.*)'\]$/, '$1')
|
id: item.id.replace(/^url:\['(.*)'\]$/, "$1"),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.length === 0 ? (
|
return data.length === 0 ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
<CardGrid maxCols={1}>
|
<CardGrid maxCols={1}>
|
||||||
<Card>
|
<Card>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<DataTable columns={columns} data={data} />
|
<DataTable columns={columns} data={data} />
|
||||||
</Card>
|
</Card>
|
||||||
</CardGrid>
|
</CardGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,36 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
export default function CardGrid({
|
export default function CardGrid({
|
||||||
maxCols: maxCols = 4,
|
maxCols = 4,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
maxCols?: number;
|
maxCols?: number;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
let baseClassName = `items-start justify-center gap-6 rounded-lg p-8 grid grid-cols-1`;
|
let baseClassName =
|
||||||
|
"items-start justify-center gap-6 rounded-lg p-8 grid grid-cols-1";
|
||||||
|
|
||||||
if (maxCols >= 2) {
|
if (maxCols >= 2) {
|
||||||
baseClassName += " lg:grid-cols-2";
|
baseClassName += " lg:grid-cols-2";
|
||||||
}
|
}
|
||||||
if (maxCols >= 3) {
|
if (maxCols >= 3) {
|
||||||
baseClassName += " xl:grid-cols-3";
|
baseClassName += " xl:grid-cols-3";
|
||||||
}
|
}
|
||||||
if (maxCols >= 4) {
|
if (maxCols >= 4) {
|
||||||
baseClassName += " 2xl:grid-cols-4";
|
baseClassName += " 2xl:grid-cols-4";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (className == undefined) {
|
if (className === undefined) {
|
||||||
className = baseClassName;
|
className = baseClassName;
|
||||||
} else {
|
} else {
|
||||||
className = baseClassName + " " + className;
|
className = `${baseClassName} ${className}`;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`${className}`} {...props}>
|
||||||
className={`${className}`}
|
{children}
|
||||||
{...props}
|
</div>
|
||||||
>
|
);
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,35 +4,35 @@ import { useTheme } from "next-themes";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
export default function ModeToggle() {
|
export default function ModeToggle() {
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon">
|
||||||
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||||
Light
|
Light
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||||
Dark
|
Dark
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
System
|
System
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
"use server";
|
"use server";
|
||||||
import postgres, { Sql } from 'postgres';
|
import postgres, { type Sql } from "postgres";
|
||||||
import { Surreal } from "surrealdb.js";
|
import { Surreal } from "surrealdb.js";
|
||||||
const db = new Surreal();
|
const db = new Surreal();
|
||||||
|
|
||||||
export async function initConnectionSurreal(): Promise<Surreal> {
|
export async function initConnectionSurreal(): Promise<Surreal> {
|
||||||
try {
|
try {
|
||||||
await db.connect("ws://" + process.env.DB_URL_PORT + "/rpc", {
|
await db.connect(`ws://${process.env.DB_URL_PORT}/rpc`, {
|
||||||
namespace: "url",
|
namespace: "url",
|
||||||
database: "url",
|
database: "url",
|
||||||
auth: {
|
auth: {
|
||||||
username: process.env.DB_USER || "root",
|
username: process.env.DB_USER || "root",
|
||||||
password: process.env.DB_PASSWORD || "root",
|
password: process.env.DB_PASSWORD || "root",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("ERROR", e);
|
console.error("ERROR", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initConnectionPostgres(): Promise<Sql<{}>> {
|
export async function initConnectionPostgres(): Promise<Sql<{}>> {
|
||||||
const DB_URL_PORT = (process.env.DB_URL_PORT || "postgres:5432").split(":");
|
const DB_URL_PORT = (process.env.DB_URL_PORT || "postgres:5432").split(":");
|
||||||
const sql = postgres({
|
const sql = postgres({
|
||||||
host: DB_URL_PORT[0],
|
host: DB_URL_PORT[0],
|
||||||
port: parseInt(DB_URL_PORT[1]),
|
port: Number.parseInt(DB_URL_PORT[1]),
|
||||||
user: process.env.POSTGRES_USER || "root",
|
user: process.env.POSTGRES_USER || "root",
|
||||||
password: process.env.POSTGRES_PASSWORD || "root",
|
password: process.env.POSTGRES_PASSWORD || "root",
|
||||||
database: process.env.POSTGRES_DB || "url",
|
database: process.env.POSTGRES_DB || "url",
|
||||||
max: 100,
|
max: 100,
|
||||||
onnotice: () => { },
|
onnotice: () => {},
|
||||||
});
|
});
|
||||||
return sql;
|
return sql;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,28 +3,29 @@ import DarkModeToggle from "@/components/dark-mode-toggle";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function Nav() {
|
export default async function Nav() {
|
||||||
return (
|
return (
|
||||||
<nav className="relative flex flex-row place-items-center gap-4 p-2 px-4 font-medium border-b">
|
<nav className="relative flex flex-row place-items-center gap-4 p-2 px-4 font-medium border-b">
|
||||||
<div className="text-2xl">
|
<div className="text-2xl">
|
||||||
<Link href="/">Next Url Shortener</Link>
|
<Link href="/">Next Url Shortener</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className="transition-colors text-foreground/50 hover:text-foreground/100">
|
{/* <div className="transition-colors text-foreground/50 hover:text-foreground/100">
|
||||||
<Link href="/create">Create</Link>
|
<Link href="/create">Create</Link>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className="transition-colors text-foreground/50 hover:text-foreground/100">
|
<div className="transition-colors text-foreground/50 hover:text-foreground/100">
|
||||||
<Link href="/stats">Stats</Link>
|
<Link href="/stats">Stats</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 items-center justify-between gap-4 md:justify-end">
|
||||||
|
<div className="transition-colors text-foreground/50 hover:text-foreground/100">
|
||||||
|
<Link href="https://github.com/NexVeridian/next-url-shortener">
|
||||||
|
GitHub
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-between gap-4 md:justify-end">
|
<DarkModeToggle />
|
||||||
<div className="transition-colors text-foreground/50 hover:text-foreground/100">
|
</div>
|
||||||
<Link href="https://github.com/NexVeridian/next-url-shortener">GitHub</Link>
|
</nav>
|
||||||
</div>
|
);
|
||||||
|
|
||||||
<DarkModeToggle />
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
import type { ThemeProviderProps } from "next-themes/dist/types";
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,78 +1,78 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{ts,tsx}',
|
"./pages/**/*.{ts,tsx}",
|
||||||
'./components/**/*.{ts,tsx}',
|
"./components/**/*.{ts,tsx}",
|
||||||
'./app/**/*.{ts,tsx}',
|
"./app/**/*.{ts,tsx}",
|
||||||
'./src/**/*.{ts,tsx}',
|
"./src/**/*.{ts,tsx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
padding: "2rem",
|
padding: "2rem",
|
||||||
screens: {
|
screens: {
|
||||||
"3xl": "1536px",
|
"3xl": "1536px",
|
||||||
"4xl": "1792px",
|
"4xl": "1792px",
|
||||||
"5xl": "2048px",
|
"5xl": "2048px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: "hsl(var(--border))",
|
||||||
input: "hsl(var(--input))",
|
input: "hsl(var(--input))",
|
||||||
ring: "hsl(var(--ring))",
|
ring: "hsl(var(--ring))",
|
||||||
background: "hsl(var(--background))",
|
background: "hsl(var(--background))",
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: "hsl(var(--foreground))",
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: "hsl(var(--primary))",
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: "hsl(var(--muted))",
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "hsl(var(--accent))",
|
DEFAULT: "hsl(var(--accent))",
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: "hsl(var(--popover))",
|
DEFAULT: "hsl(var(--popover))",
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: "hsl(var(--card))",
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: "hsl(var(--card-foreground))",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: "var(--radius)",
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: "calc(var(--radius) - 2px)",
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: "calc(var(--radius) - 4px)",
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
"accordion-down": {
|
"accordion-down": {
|
||||||
from: { height: 0 },
|
from: { height: 0 },
|
||||||
to: { height: "var(--radix-accordion-content-height)" },
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
},
|
},
|
||||||
"accordion-up": {
|
"accordion-up": {
|
||||||
from: { height: "var(--radix-accordion-content-height)" },
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
to: { height: 0 },
|
to: { height: 0 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [require("tailwindcss-animate")],
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,40 +1,27 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
"allowJs": true,
|
||||||
"dom.iterable",
|
"skipLibCheck": true,
|
||||||
"esnext"
|
"strict": true,
|
||||||
],
|
"noEmit": true,
|
||||||
"allowJs": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"module": "esnext",
|
||||||
"strict": true,
|
"moduleResolution": "bundler",
|
||||||
"noEmit": true,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"isolatedModules": true,
|
||||||
"module": "esnext",
|
"jsx": "preserve",
|
||||||
"moduleResolution": "bundler",
|
"incremental": true,
|
||||||
"resolveJsonModule": true,
|
"plugins": [
|
||||||
"isolatedModules": true,
|
{
|
||||||
"jsx": "preserve",
|
"name": "next"
|
||||||
"incremental": true,
|
}
|
||||||
"plugins": [
|
],
|
||||||
{
|
"paths": {
|
||||||
"name": "next"
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
"paths": {
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"@/*": [
|
"exclude": ["node_modules"]
|
||||||
"./src/*"
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"next-env.d.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
".next/types/**/*.ts",
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue