forked from dubinc/dub
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Dub – an open-source link shortener built with Vercel Edge Functions …
…and Upstash.
- Loading branch information
1 parent
b02b81d
commit 39b8069
Showing
22 changed files
with
3,487 additions
and
712 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
UPSTASH_REDIS_REST_URL= | ||
UPSTASH_REDIS_REST_TOKEN= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ yarn-error.log* | |
|
||
# local env files | ||
.env*.local | ||
.env | ||
|
||
# vercel | ||
.vercel | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import Image, { ImageProps } from "next/future/image"; | ||
import { useState, useEffect } from "react"; | ||
|
||
export default function BlurImage(props: ImageProps) { | ||
const [isLoading, setLoading] = useState(true); | ||
const [src, setSrc] = useState(props.src); | ||
useEffect(() => setSrc(props.src), [props.src]); // update the `src` value when the `prop.src` value changes | ||
|
||
return ( | ||
<Image | ||
{...props} | ||
src={src} | ||
alt={props.alt} | ||
className={`${props.className} ${ | ||
isLoading | ||
? "grayscale blur-2xl scale-110" | ||
: "grayscale-0 blur-0 scale-100" | ||
}`} | ||
onLoadingComplete={async () => { | ||
setLoading(false); | ||
}} | ||
onError={() => { | ||
setSrc(`https://avatar.tobi.sh/${props.alt}`); // if the image fails to load, use the default avatar | ||
}} | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { useState } from "react"; | ||
|
||
export default function CopyButton({ url }: { url: string }) { | ||
const [copied, setCopied] = useState(false); | ||
return ( | ||
<button | ||
onClick={() => { | ||
setCopied(true); | ||
navigator.clipboard.writeText(url); | ||
setTimeout(() => setCopied(false), 3000); | ||
}} | ||
className="group p-1.5 rounded-full bg-gray-100 hover:bg-blue-100 transition-all" | ||
> | ||
{copied ? ( | ||
<TickIcon className="text-gray-500 group-hover:text-blue-800 transition-all" /> | ||
) : ( | ||
<CopyIcon className="text-gray-500 group-hover:text-blue-800 transition-all" /> | ||
)} | ||
</button> | ||
); | ||
} | ||
|
||
function TickIcon({ className }: { className: string }) { | ||
return ( | ||
<svg | ||
fill="none" | ||
shapeRendering="geometricPrecision" | ||
stroke="currentColor" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
strokeWidth="1.5" | ||
viewBox="0 0 24 24" | ||
width="14" | ||
height="14" | ||
className={className} | ||
> | ||
<path d="M20 6L9 17l-5-5" /> | ||
</svg> | ||
); | ||
} | ||
|
||
function CopyIcon({ className }: { className: string }) { | ||
return ( | ||
<svg | ||
fill="none" | ||
shapeRendering="geometricPrecision" | ||
stroke="currentColor" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
strokeWidth="1.5" | ||
viewBox="0 0 24 24" | ||
width="14" | ||
height="14" | ||
className={className} | ||
> | ||
<path d="M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z" /> | ||
</svg> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import BlurImage from "@/components/blur-image"; | ||
import CopyButton from "@/components/copy-button"; | ||
import LoadingDots from "@/components/loading-dots"; | ||
import useSWR from "swr"; | ||
import fetcher from "@/lib/fetcher"; | ||
import nFormatter from "@/lib/n-formatter"; | ||
|
||
export default function LinkCard({ | ||
_key: key, | ||
url, | ||
}: { | ||
_key: string; | ||
url: string; | ||
}) { | ||
const shortURL = `${ | ||
process.env.NEXT_PUBLIC_DEMO_APP === "1" | ||
? "https://dub.sh" | ||
: process.env.VERCEL === "1" | ||
? process.env.VERCEL_URL | ||
: "http://localhost:3000" | ||
}/${key}`; // if you're self-hosting you can just replace this with your own domain | ||
|
||
const urlHostname = new URL(url).hostname; | ||
|
||
const { data: clicks, isValidating } = useSWR<string>( | ||
`/api/links/${key}/clicks`, | ||
fetcher | ||
); | ||
|
||
return ( | ||
<div | ||
key={key} | ||
className="flex items-center border border-gray-200 hover:border-black p-3 rounded-md transition-all" | ||
> | ||
<BlurImage | ||
src={`https://logo.clearbit.com/${urlHostname}`} | ||
alt={urlHostname} | ||
className="w-10 h-10 rounded-full mr-2 border border-gray-200" | ||
width={20} | ||
height={20} | ||
/> | ||
<div> | ||
<div className="flex items-center space-x-2 mb-1"> | ||
<a | ||
className="text-blue-800 font-semibold" | ||
href={shortURL} | ||
target="_blank" | ||
rel="noreferrer" | ||
> | ||
{shortURL.replace(/^https?:\/\//, "")} | ||
</a> | ||
<CopyButton url={shortURL} /> | ||
<div className="rounded-md bg-gray-100 px-2 py-0.5"> | ||
<p className="text-sm text-gray-500"> | ||
{isValidating || !clicks ? ( | ||
<LoadingDots color="#71717A" /> | ||
) : ( | ||
nFormatter(parseInt(clicks)) | ||
)}{" "} | ||
clicks | ||
</p> | ||
</div> | ||
</div> | ||
<p className="text-sm text-gray-500 truncate w-72">{url}</p> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
.loading { | ||
display: inline-flex; | ||
align-items: center; | ||
} | ||
|
||
.loading .spacer { | ||
margin-right: 2px; | ||
} | ||
|
||
.loading span { | ||
animation-name: blink; | ||
animation-duration: 1.4s; | ||
animation-iteration-count: infinite; | ||
animation-fill-mode: both; | ||
width: 5px; | ||
height: 5px; | ||
border-radius: 50%; | ||
display: inline-block; | ||
margin: 0 1px; | ||
} | ||
|
||
.loading span:nth-of-type(2) { | ||
animation-delay: 0.2s; | ||
} | ||
|
||
.loading span:nth-of-type(3) { | ||
animation-delay: 0.4s; | ||
} | ||
|
||
@keyframes blink { | ||
0% { | ||
opacity: 0.2; | ||
} | ||
20% { | ||
opacity: 1; | ||
} | ||
100% { | ||
opacity: 0.2; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import styles from "./loading-dots.module.css"; | ||
|
||
const LoadingDots = ({ color = "#000" }: { color: string }) => { | ||
return ( | ||
<span className={styles.loading}> | ||
<span style={{ backgroundColor: color }} /> | ||
<span style={{ backgroundColor: color }} /> | ||
<span style={{ backgroundColor: color }} /> | ||
</span> | ||
); | ||
}; | ||
|
||
export default LoadingDots; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export default async function fetcher<JSON = any>( | ||
input: RequestInfo, | ||
init?: RequestInit | ||
): Promise<JSON> { | ||
const res = await fetch(input, init); | ||
|
||
if (!res.ok && res.status === 401) { | ||
throw new Error("Unauthorized"); | ||
} | ||
|
||
return res.json(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
export default function nFormatter(num: number, digits?: number) { | ||
const lookup = [ | ||
{ value: 1, symbol: "" }, | ||
{ value: 1e3, symbol: "K" }, | ||
{ value: 1e6, symbol: "M" }, | ||
{ value: 1e9, symbol: "G" }, | ||
{ value: 1e12, symbol: "T" }, | ||
{ value: 1e15, symbol: "P" }, | ||
{ value: 1e18, symbol: "E" }, | ||
]; | ||
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; | ||
var item = lookup | ||
.slice() | ||
.reverse() | ||
.find(function (item) { | ||
return num >= item.value; | ||
}); | ||
return item | ||
? (num / item.value).toFixed(digits || 1).replace(rx, "$1") + item.symbol | ||
: "0"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { Redis } from "@upstash/redis"; | ||
|
||
export const redis = new Redis({ | ||
url: process.env.UPSTASH_REDIS_REST_URL || "", | ||
token: process.env.UPSTASH_REDIS_REST_TOKEN || "", | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { useEffect, useState } from "react"; | ||
|
||
const useLocalStorage = <T>( | ||
key: string, | ||
initialValue: T | ||
): [T, (value: T) => void] => { | ||
const [storedValue, setStoredValue] = useState(initialValue); | ||
|
||
useEffect(() => { | ||
// Retrieve from localStorage | ||
const item = window.localStorage.getItem(key); | ||
if (item) { | ||
setStoredValue(JSON.parse(item)); | ||
} | ||
}, [key]); | ||
|
||
const setValue = (value: any) => { | ||
// Save state | ||
setStoredValue(value); | ||
// Save to localStorage | ||
window.localStorage.setItem(key, JSON.stringify(value)); | ||
}; | ||
return [storedValue, setValue]; | ||
}; | ||
|
||
export default useLocalStorage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { NextRequest, NextFetchEvent, NextResponse } from "next/server"; | ||
import { redis } from "@/lib/redis"; | ||
|
||
export const config = { | ||
matcher: "/([^/.]*)", | ||
}; | ||
|
||
export default async function middleware(req: NextRequest, ev: NextFetchEvent) { | ||
const url = req.nextUrl.pathname; | ||
const key = url.split("/")[1]; | ||
if (key.length === 0) { | ||
return NextResponse.next(); | ||
} // skip root | ||
const target = await redis.hget<string>("links", key); | ||
if (target) { | ||
ev.waitUntil(redis.hincrby("stats", key, 1)); // increment click count | ||
return NextResponse.redirect(target); | ||
} else { | ||
return NextResponse.redirect("/"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
/** @type {import('next').NextConfig} */ | ||
module.exports = { | ||
reactStrictMode: true, | ||
} | ||
experimental: { images: { allowFutureImage: true } }, | ||
images: { | ||
domains: ["logo.clearbit.com", "avatar.tobi.sh"], | ||
}, | ||
}; |
Oops, something went wrong.