├── public
├── robots.txt
├── og-image.webp
├── x.svg
├── github.svg
├── logo.svg
└── factory.svg
├── src
├── app
│ ├── favicon.ico
│ ├── globals.css
│ ├── [slug]
│ │ └── route.ts
│ ├── about
│ │ └── page.tsx
│ ├── api
│ │ └── route.ts
│ ├── layout.tsx
│ └── page.tsx
├── lib
│ └── supabase-client.ts
└── components
│ └── HeaderNav.tsx
├── postcss.config.js
├── next-env.d.ts
├── docker-compose.yml
├── .env.example
├── next.config.js
├── .dockerignore
├── .gitignore
├── tailwind.config.ts
├── tsconfig.json
├── package.json
├── Dockerfile
├── LICENSE
├── .cursorrules
├── README.zh.md
├── README.md
└── .github
└── workflows
└── docker-push.yml
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
--------------------------------------------------------------------------------
/public/og-image.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisonwang/url-shortener/HEAD/public/og-image.webp
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisonwang/url-shortener/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
6 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | app:
3 | image: iamxiaowangye/url-shortener
4 | ports:
5 | - "3000:3000"
6 | environment:
7 | - NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
8 | - NEXT_PUBLIC_SUPABASE_KEY=${NEXT_PUBLIC_SUPABASE_KEY}
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_SUPABASE_URL=https://einoylkzpvrajgaeugpp.supabase.co
2 | NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImVpbm95bGt6cHZyYWpnYWV1Z3BwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Mjc0ODg0MjIsImV4cCI6MjA0MzA2NDQyMn0.kFLfPsDt0g-decx9AFWRiLDN1-69WLSV6XHQGO0Yk4o
3 |
--------------------------------------------------------------------------------
/public/x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/supabase-client.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@supabase/supabase-js';
2 |
3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'http://placeholder-url.com';
4 | const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_KEY || 'placeholder-key';
5 |
6 | export const supabase = createClient(supabaseUrl, supabaseKey);
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const nextConfig = {
2 | reactStrictMode: true,
3 | webpack: (config, { isServer }) => {
4 | if (!isServer) {
5 | config.resolve.fallback = {
6 | ...config.resolve.fallback,
7 | fs: false,
8 | };
9 | }
10 | return config;
11 | },
12 | }
13 |
14 | module.exports = nextConfig
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Node.js 依赖目录
2 | node_modules
3 |
4 | # Next.js 构建输出
5 | .next
6 |
7 | # 环境变量文件
8 | .env*
9 |
10 | # 日志文件
11 | *.log
12 |
13 | # 操作系统生成的文件
14 | .DS_Store
15 | Thumbs.db
16 |
17 | # 编辑器配置文件
18 | .vscode
19 | .idea
20 |
21 | # Git 相关文件
22 | .git
23 | .gitignore
24 |
25 | # 测试文件
26 | __tests__
27 | *.test.js
28 | *.spec.js
29 |
30 | # 其他不需要的文件
31 | README.md
32 | LICENSE
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node.js
2 | node_modules/
3 | npm-debug.log
4 |
5 | # Next.js
6 | .next/
7 | out/
8 |
9 | # Environment variables
10 | .env
11 |
12 | # Logs
13 | logs/
14 | *.log
15 |
16 | # OS generated files
17 | .DS_Store
18 | Thumbs.db
19 |
20 | # Editor files
21 | .vscode/
22 | .idea/
23 |
24 | # Build files
25 | dist/
26 | build/
27 |
28 | # Temporary files
29 | *.tmp
30 | *.swp
31 |
32 | # Other
33 | *.bak
34 | *.orig
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer utilities {
6 | .bg-factory {
7 | background-color: #E2E1CF;
8 | }
9 | }
10 |
11 | body {
12 | @apply bg-factory text-neutral-800;
13 | }
14 |
15 |
16 |
17 | body {
18 | font-family: 'Comic Sans MS', sans-serif;
19 | }
20 |
21 | /* 自定义滚动条样式 */
22 | .custom-scrollbar::-webkit-scrollbar {
23 | width: 8px;
24 | }
25 |
26 | .custom-scrollbar::-webkit-scrollbar-track {
27 | background: #DAD9C4;
28 | }
29 |
30 | .custom-scrollbar::-webkit-scrollbar-thumb {
31 | background: #C3C2AC;
32 | border-radius: 4px;
33 | }
34 |
35 | .custom-scrollbar::-webkit-scrollbar-thumb:hover {
36 | background: #555;
37 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "url-shortener",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "next": "^14.2.8",
13 | "react": "^18",
14 | "react-dom": "^18",
15 | "nanoid": "^3.3.4",
16 | "@supabase/supabase-js": "^2.21.0"
17 | },
18 | "devDependencies": {
19 | "@types/node": "^20",
20 | "@types/react": "^18",
21 | "@types/react-dom": "^18",
22 | "autoprefixer": "^10",
23 | "eslint": "^8",
24 | "eslint-config-next": "13.5.6",
25 | "postcss": "^8",
26 | "tailwindcss": "^3",
27 | "typescript": "^5"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/public/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # 构建阶段
2 | FROM node:20-alpine AS builder
3 |
4 | # 设置工作目录
5 | WORKDIR /app
6 |
7 | # 复制package.json和package-lock.json
8 | COPY package*.json ./
9 |
10 | # 安装所有依赖
11 | RUN npm ci
12 |
13 | # 复制项目文件
14 | COPY . .
15 |
16 | # 构建项目
17 | RUN npm run build
18 |
19 | # 生产阶段
20 | FROM node:20-alpine
21 |
22 | # 设置工作目录
23 | WORKDIR /app
24 |
25 | # 复制package.json和package-lock.json
26 | COPY package*.json ./
27 |
28 | # 安装生产依赖
29 | RUN npm ci --only=production
30 |
31 | # 复制构建产物和公共文件
32 | COPY --from=builder /app/.next ./.next
33 | COPY --from=builder /app/public ./public
34 |
35 | # 创建启动脚本
36 | RUN echo '#!/bin/sh' > start.sh && \
37 | echo 'echo "NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL" > .env' >> start.sh && \
38 | echo 'echo "NEXT_PUBLIC_SUPABASE_KEY=$NEXT_PUBLIC_SUPABASE_KEY" >> .env' >> start.sh && \
39 | echo 'npm start' >> start.sh && \
40 | chmod +x start.sh
41 |
42 | # 暴露端口
43 | EXPOSE 3000
44 |
45 | # 设置启动命令
46 | CMD ["/bin/sh", "/app/start.sh"]
47 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 小王爷
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/app/[slug]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { supabase } from '@/lib/supabase-client';
3 |
4 | export async function GET(
5 | request: NextRequest,
6 | { params }: { params: { slug: string } }
7 | ) {
8 | try {
9 | const { slug } = params;
10 |
11 | // Query links table to get original URL
12 | const { data, error } = await supabase
13 | .from('links')
14 | .select('url')
15 | .eq('slug', slug)
16 | .single();
17 |
18 | if (error || !data) {
19 | return NextResponse.json({ error: 'Short link does not exist' }, { status: 404 });
20 | }
21 |
22 | // Record access log
23 | const ua = request.headers.get('user-agent') || '';
24 | const ip = request.ip || request.headers.get('x-forwarded-for') || '';
25 | const referer = request.headers.get('referer') || '';
26 |
27 | await supabase.from('logs').insert({
28 | url: data.url,
29 | slug,
30 | referer,
31 | ua,
32 | ip
33 | });
34 |
35 | // 302 redirect to original URL
36 | return NextResponse.redirect(data.url, 302);
37 |
38 | } catch (error) {
39 | console.error('Redirect failed:', error);
40 | return NextResponse.json({ error: 'Redirect failed' }, { status: 500 });
41 | }
42 | }
--------------------------------------------------------------------------------
/.cursorrules:
--------------------------------------------------------------------------------
1 | # Full-Stack Web Developer
2 |
3 | You are an expert full-stack web developer focused on producing clear, readable Next.js code.
4 |
5 | You always use the latest stable versions of Next.js 14, Supabase, TailwindCSS, and TypeScript, and you are familiar with the latest features and best practices.
6 |
7 | You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning.
8 |
9 | Technical preferences:
10 |
11 | - Always use kebab-case for component names (e.g. my-component.tsx)
12 | - Favour using React Server Components and Next.js SSR features where possible
13 | - Minimize the usage of client components ('use client') to small, isolated components
14 | - Always add loading and error states to data fetching components
15 | - Implement error handling and error logging
16 | - Use semantic HTML elements where possible
17 |
18 | General preferences:
19 |
20 | - Follow the user's requirements carefully & to the letter.
21 | - Always write correct, up-to-date, bug-free, fully functional and working, secure, performant and efficient code.
22 | - Focus on readability over being performant.
23 | - Fully implement all requested functionality.
24 | - Leave NO todo's, placeholders or missing pieces in the code.
25 | - Be sure to reference file names.
26 | - Be concise. Minimize any other prose.
27 | - If you think there might not be a correct answer, you say so. If you do not know the answer, say so instead of guessing.
28 |
--------------------------------------------------------------------------------
/src/components/HeaderNav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from 'next/link';
4 | import { useEffect, useState } from 'react';
5 |
6 | export default function HeaderNav() {
7 | const [isScrolled, setIsScrolled] = useState(false);
8 |
9 | useEffect(() => {
10 | const handleScroll = () => {
11 | if (window.scrollY > 0) {
12 | setIsScrolled(true);
13 | } else {
14 | setIsScrolled(false);
15 | }
16 | };
17 |
18 | window.addEventListener('scroll', handleScroll);
19 | return () => {
20 | window.removeEventListener('scroll', handleScroll);
21 | };
22 | }, []);
23 |
24 | return (
25 |
26 |
43 |
44 | );
45 | }
--------------------------------------------------------------------------------
/src/app/about/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Link from 'next/link';
4 |
5 | export default function AboutPage() {
6 | return (
7 |
8 |
9 |
About URL Shortener
10 |
11 |
12 |
What is URL Shortener?
13 |
14 | URL Shortener is a free tool designed to help users easily shorten long URLs. Our goal is to make URL shortening simple, fast, and efficient. Whether you're a marketer, developer, or anyone who needs to share links, URL Shortener can help you create concise, manageable links.
15 |
16 |
Features
17 |
18 |
Instant URL shortening
19 |
Custom short links
20 |
Analytics for link clicks
21 |
Secure and reliable redirection
22 |
Free to use
23 |
24 |
How to Use
25 |
26 |
Enter your long URL in the input field on the home page
27 |
Optional: Enter your desired short link
28 |
Click the "Generate Short Link" button
29 |
Copy your shortened URL and share it
30 |
31 |
About the Author
32 |
33 | This website was developed by Harrison Wang. While not a professional developer, I successfully created this tool with the help of AI. The entire project was developed using Cursor and Theme originally by Viggo.
34 |