├── .gitignore
├── LICENSE
├── README.md
├── app
├── .well-known
│ └── farcaster.json
│ │ └── route.ts
├── CommitMono-400-Regular.otf
├── CommitMono-700-Regular.otf
├── api
│ ├── content
│ │ └── [cid]
│ │ │ └── route.ts
│ ├── languages
│ │ └── route.ts
│ ├── preview
│ │ └── [slug]
│ │ │ └── route.tsx
│ └── upload
│ │ └── route.ts
├── favicon.ico
├── globals.css
├── install
│ └── route.ts
├── layout.tsx
├── page.tsx
└── snip
│ └── [slug]
│ ├── layout.tsx
│ └── page.tsx
├── components.json
├── components
├── code-form.tsx
├── footer.tsx
├── header.tsx
├── password-check.tsx
├── password-content.tsx
├── read-only-editor.tsx
├── share-modal.tsx
└── ui
│ ├── button.tsx
│ ├── card.tsx
│ ├── checkbox.tsx
│ ├── dialog.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── select.tsx
│ └── separator.tsx
├── lib
├── default.ts
├── languages.ts
└── utils.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── CommitMono-400-Regular.otf
├── CommitMono-700-Regular.otf
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── bmc.png
├── ethereum.svg
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── icon.png
├── og.png
├── pinata.png
├── script.sh
├── site.webmanifest
└── vercel-icon-dark.svg
├── tailwind.config.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | migrate.ts
38 | files/
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Steven Simkins
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Snippets.so
2 | 
3 | No ads, no fuss, just code
4 |
5 | ## Development Setup
6 |
7 | Clone the repo and install
8 |
9 | ```
10 | git clone https://github.com/stevedylandev/snippets && cd snippets && npm install
11 | ```
12 |
13 | Create a `.env.local` file with the following variables
14 |
15 | ```
16 | # Created at https://app.pinata.cloud/developers/api-keys
17 | PINATA_JWT=
18 |
19 | # Located at https://app.pinata.cloud/gateway
20 | GATEWAY_URL=
21 | ```
22 |
23 | Start up the server with `npm run dev`
24 |
25 | ## Ports
26 |
27 | ### API
28 |
29 | ```bash
30 | curl --location 'https://www.snippets.so/api/upload' \
31 | --header 'Content-Type: application/json' \
32 | --data '{
33 | "content": "console.log(\"hello world!\")",
34 | "name": "hello.ts",
35 | "lang": "typescript"
36 | }'
37 |
38 | ```
39 | [List of supported languages](https://github.com/stevedylandev/snippets/blob/main/lib/languages.ts)
40 |
41 | ```
42 | https://snippets.so/snip/{IpfsHash}
43 | ```
44 |
45 | ### CLI
46 |
47 | ```
48 | brew install stevedylandev/snippets-cli/snippets-cli
49 | ```
50 |
51 | For other installs check out the [Github repo](https://github.com/stevedylandev/snippets-cli)
52 |
53 | **Usage**
54 |
55 | ```
56 | snip hello.ts
57 | ```
58 |
59 | ## Contact
60 |
61 | Feedback? [hello@stevedylan.dev](mailto:hello@stevedylan.dev)
62 | Like what you see? [Eth Address](https://rainbow.me/stevedylandev.eth) [Buy me a coffee](https://buymeacoffee.com/stevedylandev)
63 |
--------------------------------------------------------------------------------
/app/.well-known/farcaster.json/route.ts:
--------------------------------------------------------------------------------
1 | export async function GET() {
2 | const appUrl = process.env.NEXT_PUBLIC_URL || "https://snippets.so"
3 |
4 | const config = {
5 | accountAssociation: {
6 | header:
7 | "eyJmaWQiOjYwMjMsInR5cGUiOiJjdXN0b2R5Iiwia2V5IjoiMHg0NTYxMzExNjFmODNDN0Q3ZkRBMTViMzJhNWY3QzIxRkQ0RTI3RTk2In0",
8 | payload: "eyJkb21haW4iOiJzbmlwcGV0cy5zbyJ9",
9 | signature:
10 | "MHgxYjE4ZGYzNTk0OTZkZmY0NGU3Nzg3YWY1NTc3NmE3YzExMzk5OWNmYjU2MWY4OTUxOGQyYTlkYTAxNWU3YmFmMTg5MTFlZTI0YzZmYzA1ZWI0NDBmM2EzNmU1ODE3ZWRjYjQwMTI3M2UxMDUzZWFmNjc5OGM0MWY0MjkyM2VmNjFj",
11 | },
12 | frame: {
13 | version: "1",
14 | name: "Snippets.so",
15 | iconUrl: `${appUrl}/icon.png`,
16 | homeUrl: appUrl,
17 | imageUrl: `${appUrl}/og.png`,
18 | buttonTitle: "Share Snippet",
19 | splashImageUrl: `${appUrl}/icon.png`,
20 | splashBackgroundColor: "#ffffff",
21 | subtitle: "Clean and Simple Code Snippets",
22 | description: "Quickly and easily share code snippets on Farcaster",
23 | primaryCategory: "developer-tools",
24 | tags: [
25 | "developer-tools",
26 | "code",
27 | "snippets"
28 | ],
29 | heroImageUrl: "https://snippets.so/og.png",
30 | tagline: "Clean and Simple Code Snippets",
31 | ogTitle: "Snippets",
32 | ogDescription: "Clean and Simple Code Snippers",
33 | ogImageUrl: "https://snippets.so/og.png"
34 | },
35 | };
36 |
37 | return Response.json(config);
38 | }
39 |
--------------------------------------------------------------------------------
/app/CommitMono-400-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/app/CommitMono-400-Regular.otf
--------------------------------------------------------------------------------
/app/CommitMono-700-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/app/CommitMono-700-Regular.otf
--------------------------------------------------------------------------------
/app/api/content/[cid]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { PinataSDK } from "pinata";
3 | import * as argon2 from "argon2";
4 |
5 | const pinata = new PinataSDK({
6 | pinataJwt: process.env.PINATA_JWT,
7 | pinataGateway: process.env.GATEWAY_DOMAIN,
8 | });
9 |
10 | export async function POST(
11 | request: Request,
12 | { params }: { params: { cid: string } },
13 | ) {
14 | try {
15 | const body = await request.json();
16 | const signedUrl = await pinata.gateways.createSignedURL({
17 | cid: params.cid,
18 | expires: 20,
19 | });
20 | const contentReq = await fetch(signedUrl);
21 | const content = await contentReq.text();
22 |
23 | const fileInfo = await pinata.files.list().cid(params.cid);
24 | const file = fileInfo.files[0];
25 |
26 | const password = String(body.password);
27 | const hash = String(file.keyvalues.passwordHash);
28 |
29 | try {
30 | const isValid = await argon2.verify(hash, password);
31 | if (isValid) {
32 | return NextResponse.json({ content });
33 | }
34 | } catch (verifyError) {
35 | console.error("Verification error:", verifyError);
36 | }
37 |
38 | return NextResponse.json({ error: "Invalid password" }, { status: 401 });
39 | } catch (error) {
40 | return NextResponse.json({ error }, { status: 500 });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/api/languages/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { languages } from "@/lib/languages";
3 |
4 | export async function GET() {
5 | try {
6 | return NextResponse.json(languages);
7 | } catch (error) {
8 | console.log(error);
9 | return NextResponse.json(error);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/api/preview/[slug]/route.tsx:
--------------------------------------------------------------------------------
1 | import { NextRequest } from "next/server";
2 | import { PinataSDK } from "pinata";
3 | import { ImageResponse } from 'next/og';
4 |
5 | const pinata = new PinataSDK({
6 | pinataJwt: process.env.PINATA_JWT,
7 | pinataGateway: process.env.GATEWAY_DOMAIN,
8 | });
9 |
10 | export const runtime = 'edge';
11 |
12 | async function loadGoogleFont(font: string, text: string, weight = 400) {
13 | const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}&text=${encodeURIComponent(text)}`
14 | const css = await (await fetch(url)).text()
15 | const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)
16 |
17 | if (resource) {
18 | const response = await fetch(resource[1])
19 | if (response.status == 200) {
20 | return await response.arrayBuffer()
21 | }
22 | }
23 |
24 | throw new Error('failed to load font data')
25 | }
26 |
27 | export async function GET(
28 | request: NextRequest,
29 | { params }: { params: { slug: string } }
30 | ) {
31 | try {
32 | const slug = params.slug;
33 |
34 | // Get file info based on slug
35 | let fileInfo;
36 | let cid;
37 |
38 | if (slug.startsWith("bafk") || slug.startsWith("Qm")) {
39 | fileInfo = await pinata.files.list().cid(slug);
40 | cid = slug;
41 | } else {
42 | fileInfo = await pinata.files.list().metadata({
43 | slug: slug,
44 | });
45 | if (fileInfo.files.length === 0) {
46 | return new Response('Snippet not found', { status: 404 });
47 | }
48 | cid = fileInfo.files[0].cid;
49 | }
50 |
51 | const file = fileInfo.files[0];
52 | console.log(file)
53 |
54 | // Get content
55 | const signedUrl = await pinata.gateways.createSignedURL({
56 | cid: cid,
57 | expires: 20,
58 | });
59 |
60 | const contentReq = await fetch(signedUrl);
61 | const content = await contentReq.text();
62 |
63 | // If password protected, don't show content
64 | const isPasswordProtected = !!file.keyvalues.passwordHash;
65 |
66 | // Get language for syntax highlighting colors
67 | const lang = file.keyvalues.lang || "javascript";
68 |
69 | // Extract first few lines (up to 10)
70 | const lines = content.split('\n').slice(0, 30);
71 | const previewContent = isPasswordProtected
72 | ? "🔒 Password protected snippet"
73 | : lines.join('\n');
74 |
75 | const text = "Snippets.so"
76 |
77 | return new ImageResponse(
78 | (
79 |
91 | {/* Header */}
92 |
104 |
111 | {text}
112 |
113 |
114 |
115 |
130 |
139 |
156 | {file.name || 'Untitled Snippet'}
157 |
158 |
159 |
175 | {previewContent}
176 | {lines.length >= 20 && !isPasswordProtected && (
177 |
...
178 | )}
179 |
180 |
181 |
182 | ),
183 | {
184 | width: 1200,
185 | height: 630,
186 | fonts: [
187 | {
188 | name: 'Space Mono',
189 | data: await loadGoogleFont('Space+Mono', text),
190 | style: 'normal',
191 | weight: 400,
192 | },
193 | {
194 | name: 'Space Mono',
195 | data: await loadGoogleFont('Space+Mono', text, 700),
196 | style: 'normal',
197 | weight: 700
198 | },
199 | ],
200 | }
201 | );
202 |
203 | } catch (error) {
204 | console.error("Error generating preview:", error);
205 | return new Response('Error generating preview', { status: 500 });
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/app/api/upload/route.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { NextResponse } from "next/server";
4 | import type { NextRequest } from "next/server";
5 | import { PinataSDK, type UploadResponse } from "pinata";
6 | const argon2 = require("argon2");
7 | import { nanoid } from "nanoid";
8 |
9 | const pinata = new PinataSDK({
10 | pinataJwt: process.env.PINATA_JWT,
11 | });
12 |
13 | export async function POST(request: NextRequest) {
14 | try {
15 | const body = await request.json();
16 | const file = new File([body.content], body.name, { type: "text/plain" });
17 | let passwordHash = "";
18 | if (body.password) {
19 | passwordHash = await argon2.hash(body.password);
20 | }
21 | const res: UploadResponse = await pinata.upload
22 | .file(file)
23 | .addMetadata({
24 | name: body.name,
25 | keyvalues: {
26 | lang: body.lang,
27 | private: body.isPrivate,
28 | expires: body.expires,
29 | passwordHash: passwordHash,
30 | },
31 | })
32 | .group(body.isPrivate === "true" ? "" : process.env.GROUP_ID || "");
33 |
34 | const updated = await pinata.files.update({
35 | id: res.id,
36 | keyvalues: {
37 | slug: nanoid(10),
38 | },
39 | });
40 | return NextResponse.json({
41 | IpfsHash: res.cid,
42 | slug: updated.keyvalues.slug,
43 | });
44 | } catch (error) {
45 | console.log(error);
46 | return NextResponse.json(error);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .cm-focused {
6 | @apply outline-none !important;
7 | }
8 |
9 | .cm-editor .cm-content {
10 | @apply font-commitMono text-sm;
11 | }
12 |
13 | @layer base {
14 | :root {
15 | --background: 0 0% 100%;
16 | --foreground: 224 71.4% 4.1%;
17 |
18 | --card: 0 0% 100%;
19 | --card-foreground: 224 71.4% 4.1%;
20 |
21 | --popover: 0 0% 100%;
22 | --popover-foreground: 224 71.4% 4.1%;
23 |
24 | --primary: 220.9 39.3% 11%;
25 | --primary-foreground: 210 20% 98%;
26 |
27 | --secondary: 220 14.3% 95.9%;
28 | --secondary-foreground: 220.9 39.3% 11%;
29 |
30 | --muted: 220 14.3% 95.9%;
31 | --muted-foreground: 220 8.9% 46.1%;
32 |
33 | --accent: 220 14.3% 95.9%;
34 | --accent-foreground: 220.9 39.3% 11%;
35 |
36 | --destructive: 0 84.2% 60.2%;
37 | --destructive-foreground: 210 20% 98%;
38 |
39 | --border: 220 13% 91%;
40 | --input: 220 13% 91%;
41 | --ring: 224 71.4% 4.1%;
42 |
43 | --radius: 0.5rem;
44 | }
45 |
46 | .dark {
47 | --background: 224 71.4% 4.1%;
48 | --foreground: 210 20% 98%;
49 |
50 | --card: 224 71.4% 4.1%;
51 | --card-foreground: 210 20% 98%;
52 |
53 | --popover: 224 71.4% 4.1%;
54 | --popover-foreground: 210 20% 98%;
55 |
56 | --primary: 210 20% 98%;
57 | --primary-foreground: 220.9 39.3% 11%;
58 |
59 | --secondary: 215 27.9% 16.9%;
60 | --secondary-foreground: 210 20% 98%;
61 |
62 | --muted: 215 27.9% 16.9%;
63 | --muted-foreground: 217.9 10.6% 64.9%;
64 |
65 | --accent: 215 27.9% 16.9%;
66 | --accent-foreground: 210 20% 98%;
67 |
68 | --destructive: 0 62.8% 30.6%;
69 | --destructive-foreground: 210 20% 98%;
70 |
71 | --border: 215 27.9% 16.9%;
72 | --input: 215 27.9% 16.9%;
73 | --ring: 216 12.2% 83.9%;
74 | }
75 | }
76 |
77 | @layer base {
78 | * {
79 | @apply border-border;
80 | }
81 | body {
82 | @apply bg-background text-foreground;
83 | }
84 | }
85 |
86 | @layer utilities {
87 | /* Hide scrollbar for Chrome, Safari and Opera */
88 | .no-scrollbar::-webkit-scrollbar {
89 | display: none;
90 | }
91 | /* Hide scrollbar for IE, Edge and Firefox */
92 | .no-scrollbar {
93 | -ms-overflow-style: none; /* IE and Edge */
94 | scrollbar-width: none; /* Firefox */
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/app/install/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export async function GET() {
4 | try {
5 | const data = await fetch("https://www.snippets.so/script.sh");
6 | const file = await data.text();
7 | const headers = new Headers();
8 | headers.set("Content-Type", "text/plain");
9 | headers.set("Content-Disposition", 'attachment; filename="install.sh"');
10 | return new NextResponse(file, {
11 | status: 200,
12 | headers: headers,
13 | });
14 | } catch (error) {
15 | console.log(error);
16 | return NextResponse.json(error);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import localFont from "next/font/local";
4 | import "./globals.css";
5 | import { Analytics } from "@vercel/analytics/react";
6 |
7 | const inter = Inter({ subsets: ["latin"] });
8 |
9 | const commitMono = localFont({
10 | src: [
11 | {
12 | path: "./CommitMono-400-Regular.otf",
13 | weight: "400",
14 | style: "normal",
15 | },
16 | {
17 | path: "./CommitMono-700-Regular.otf",
18 | weight: "700",
19 | style: "normal",
20 | },
21 | ],
22 | display: "swap",
23 | variable: "--font-commitMono",
24 | });
25 |
26 | const frame = {
27 | version: "next",
28 | imageUrl: `https://snippets.so/og.png`,
29 | button: {
30 | title: "Share Snippet",
31 | action: {
32 | type: "launch_frame",
33 | name: "Snippets.so",
34 | url: `https://snippets.so`,
35 | splashImageUrl: `https://snippets.so/icon.png`,
36 | splashBackgroundColor: "#ffffff",
37 | },
38 | },
39 | };
40 |
41 | export const metadata: Metadata = {
42 | title: "Snippets.so",
43 | description: "Clean and simple code sharing",
44 | icons: {
45 | apple: "/apple-touch-icon.png",
46 | shortcut: "/favicon.ico",
47 | icon: "/favicon-32z32.png",
48 | },
49 | openGraph: {
50 | title: "Snippets.so",
51 | description: "Clean and simple code sharing",
52 | url: "https://snippets.so",
53 | siteName: "Snippets.so",
54 | images: ["https://www.snippets.so/og.png"],
55 | },
56 | twitter: {
57 | card: "summary_large_image",
58 | title: "Snippets.so",
59 | description: "Clean and simple code sharing",
60 | images: ["https://www.snippets.so/og.png"],
61 | },
62 | other: {
63 | "fc:frame": JSON.stringify(frame),
64 | },
65 | };
66 |
67 | export default function RootLayout({
68 | children,
69 | }: Readonly<{
70 | children: React.ReactNode;
71 | }>) {
72 | return (
73 |
74 |
75 | {children}
76 |
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { CodeForm } from "@/components/code-form";
2 | import { Footer } from "@/components/footer";
3 | import { Header } from "@/components/header";
4 |
5 | export default function Home() {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/app/snip/[slug]/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import localFont from "next/font/local";
4 | import "../../globals.css";
5 | import { Analytics } from "@vercel/analytics/react";
6 | import { PinataSDK } from "pinata";
7 |
8 | const inter = Inter({ subsets: ["latin"] });
9 |
10 | const commitMono = localFont({
11 | src: [
12 | {
13 | path: "../../CommitMono-400-Regular.otf",
14 | weight: "400",
15 | style: "normal",
16 | },
17 | {
18 | path: "../../CommitMono-700-Regular.otf",
19 | weight: "700",
20 | style: "normal",
21 | },
22 | ],
23 | display: "swap",
24 | variable: "--font-commitMono",
25 | });
26 |
27 | const pinata = new PinataSDK({
28 | pinataJwt: process.env.PINATA_JWT,
29 | pinataGateway: process.env.GATEWAY_DOMAIN,
30 | });
31 |
32 | // Generate metadata based on the slug parameter
33 | export async function generateMetadata(
34 | { params }: { params: { slug: string } }
35 | ): Promise {
36 | const slug = params.slug;
37 |
38 | // You can fetch snippet details here similar to page.tsx
39 | // This is a simplified version
40 | let snippetName = "Snippets.so";
41 | let description = "Clean and simple code sharing";
42 |
43 | try {
44 | // Try to get file info based on slug
45 | const fileInfo = await pinata.files.list().metadata({
46 | slug: slug
47 | });
48 |
49 | if (fileInfo.files.length > 0) {
50 | const file = fileInfo.files[0];
51 | snippetName = file.name || "Snippets.so";
52 | description = `${snippetName}`;
53 | }
54 | } catch (error) {
55 | console.log("Error fetching metadata:", error);
56 | }
57 |
58 | const frame = {
59 | version: "next",
60 | imageUrl: `https://snippets.so/api/preview/${slug}`,
61 | button: {
62 | title: "Open Snippet",
63 | action: {
64 | type: "launch_frame",
65 | name: "Snippets.so",
66 | url: `https://snippets.so/snip/${slug}`,
67 | splashImageUrl: `https://snippets.so/icon.png`,
68 | splashBackgroundColor: "#ffffff",
69 | },
70 | },
71 | };
72 |
73 | return {
74 | title: snippetName,
75 | description: description,
76 | icons: {
77 | apple: "/apple-touch-icon.png",
78 | shortcut: "/favicon.ico",
79 | icon: "/favicon-32z32.png",
80 | },
81 | openGraph: {
82 | title: snippetName,
83 | description: description,
84 | url: `https://snippets.so/snip/${slug}`,
85 | siteName: "Snippets.so",
86 | images: [`https://www.snippets.so/api/preview/${slug}`],
87 | },
88 | twitter: {
89 | card: "summary_large_image",
90 | title: snippetName,
91 | description: description,
92 | images: [`https://www.snippets.so/api/preview/${slug}`],
93 | },
94 | other: {
95 | "fc:frame": JSON.stringify(frame),
96 | },
97 | };
98 | }
99 |
100 | export default function RootLayout({
101 | children,
102 | }: Readonly<{
103 | children: React.ReactNode;
104 | }>) {
105 | return (
106 |
107 |
108 | {children}
109 |
110 |
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/app/snip/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Header } from "@/components/header";
2 | import { ReadOnlyEditor } from "@/components/read-only-editor";
3 | import { type FileListResponse, PinataSDK } from "pinata";
4 | import type { LanguageName } from "@uiw/codemirror-extensions-langs";
5 | import { ProtectedContent } from "@/components/password-content";
6 |
7 | interface SnippetData {
8 | content: string;
9 | name: string;
10 | lang: LanguageName;
11 | expires: string;
12 | date: string;
13 | passwordHash: string;
14 | slug: string;
15 | cid: string;
16 | }
17 |
18 | const pinata = new PinataSDK({
19 | pinataJwt: process.env.PINATA_JWT,
20 | pinataGateway: process.env.GATEWAY_DOMAIN,
21 | });
22 |
23 | async function fetchData(hash: string): Promise {
24 | try {
25 | let fileInfo: FileListResponse;
26 | let cid: string;
27 | if (hash.startsWith("bafk") || hash.startsWith("Qm")) {
28 | fileInfo = await pinata.files.list().cid(hash);
29 | cid = hash;
30 | } else {
31 | fileInfo = await pinata.files.list().metadata({
32 | slug: hash,
33 | });
34 | console.log(fileInfo);
35 | cid = fileInfo.files[0].cid;
36 | }
37 | const file = fileInfo.files[0];
38 | const { data: content, contentType } = await pinata.gateways.get(cid);
39 | const creationDate = new Date(file.created_at);
40 | const cutoffDate = new Date("2024-11-07T05:02:00.939309Z");
41 | if (creationDate < cutoffDate) {
42 | const jsonContent =
43 | typeof content === "string" ? JSON.parse(content) : content;
44 |
45 | const res: SnippetData = {
46 | content: jsonContent.content,
47 | name: file.name as string,
48 | lang: jsonContent.lang as LanguageName,
49 | expires: file.keyvalues.expires || "0",
50 | date: file.created_at,
51 | passwordHash: "",
52 | slug: "",
53 | cid: cid,
54 | };
55 | console.log(res);
56 | return res;
57 | }
58 | const signedUrl = await pinata.gateways.createSignedURL({
59 | cid: cid,
60 | expires: 20,
61 | });
62 | const contentReq = await fetch(signedUrl);
63 | const rawContent = await contentReq.text();
64 | const res: SnippetData = {
65 | content: rawContent as string,
66 | name: file.name as string,
67 | lang: file.keyvalues.lang as LanguageName,
68 | expires: file.keyvalues.expires || "0",
69 | date: file.created_at,
70 | passwordHash: file.keyvalues.passwordHash,
71 | slug: file.keyvalues.slug,
72 | cid: cid,
73 | };
74 | console.log(res);
75 | return res;
76 | } catch (error) {
77 | console.log(error);
78 | return error as Error;
79 | }
80 | }
81 |
82 | export default async function Page({ params }: { params: { slug: string } }) {
83 | const slug = params.slug;
84 | const data = await fetchData(slug);
85 | let hasExpired = false;
86 | let futureDate: Date | undefined;
87 |
88 | if (data instanceof Error) {
89 | throw data;
90 | }
91 |
92 | if (data.expires !== "0") {
93 | const date = new Date(data.date);
94 | futureDate = new Date(
95 | date.getTime() + Number.parseInt(data.expires) * 1000,
96 | );
97 | const currentDate = new Date();
98 | hasExpired = currentDate > futureDate;
99 | console.log("Has expired:", hasExpired);
100 | }
101 |
102 | // Don't pass content to client if password protected
103 | const isPasswordProtected = !!data.passwordHash;
104 | const clientData = {
105 | ...data,
106 | content: isPasswordProtected ? "" : data.content,
107 | passwordHash: data.passwordHash,
108 | };
109 |
110 | return (
111 |
112 |
113 | {!hasExpired && !isPasswordProtected && (
114 |
122 | )}
123 | {!hasExpired && isPasswordProtected && (
124 |
130 | )}
131 | {hasExpired && (
132 |
133 |
148 |
Snippet Expired
149 |
150 | )}
151 |
152 | );
153 | }
154 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "gray",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/code-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useMemo, useCallback, useEffect } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { Card } from "@/components/ui/card";
6 | import { Input } from "@/components/ui/input";
7 | import CodeMirror from "@uiw/react-codemirror";
8 | import { githubLight } from "@uiw/codemirror-theme-github";
9 | import { useRouter } from "next/navigation";
10 | import { defaultCode } from "@/lib/default";
11 | import {
12 | CheckIcon,
13 | ReloadIcon,
14 | EyeOpenIcon,
15 | EyeClosedIcon,
16 | } from "@radix-ui/react-icons";
17 | import {
18 | Select,
19 | SelectContent,
20 | SelectItem,
21 | SelectTrigger,
22 | SelectValue,
23 | } from "@/components/ui/select";
24 | import { loadLanguage } from "@uiw/codemirror-extensions-langs";
25 | import type { LanguageName } from "@uiw/codemirror-extensions-langs";
26 | import { languages } from "@/lib/languages";
27 | import sdk from '@farcaster/frame-sdk';
28 |
29 |
30 | type CodeFormProps = {
31 | readOnly: boolean;
32 | content: string;
33 | };
34 |
35 | const times = [
36 | {
37 | displayName: "No Expiration",
38 | value: "0",
39 | },
40 | {
41 | displayName: "10 Minutes",
42 | value: "600",
43 | },
44 | {
45 | displayName: "1 Hour",
46 | value: "3600",
47 | },
48 | {
49 | displayName: "10 Hours",
50 | value: "36000",
51 | },
52 | {
53 | displayName: "1 Day",
54 | value: "86400",
55 | },
56 | {
57 | displayName: "10 Days",
58 | value: "864000",
59 | },
60 | ];
61 |
62 | export function CodeForm({ readOnly, content }: CodeFormProps) {
63 | const [value, setValue] = useState(defaultCode);
64 | const [name, setName] = useState("file");
65 | const [loading, setLoading] = useState(false);
66 | const [complete, setComplete] = useState(false);
67 | const [lang, setLang] = useState("tsx");
68 | const [time, setTime] = useState("0");
69 | const [password, setPassword] = useState("");
70 | const [showPassword, setShowPassword] = useState(false);
71 | const router = useRouter();
72 | const [isSDKLoaded, setIsSDKLoaded] = useState(false);
73 |
74 | const languageExtension = useMemo(() => {
75 | const extension = loadLanguage(lang);
76 | return extension ? [extension] : [];
77 | }, [lang]);
78 |
79 | const onChange = useCallback((val: string) => {
80 | console.log("val:", val);
81 | setValue(val);
82 | }, []);
83 |
84 | async function submitHandler() {
85 | try {
86 | setLoading(true);
87 | let isPrivate = "true";
88 | if (time === "0" || password === "") {
89 | isPrivate = "false";
90 | }
91 | const body = JSON.stringify({
92 | content: value,
93 | name: name,
94 | lang: lang,
95 | isPrivate: isPrivate,
96 | expires: time,
97 | password: password || "",
98 | });
99 | const req = await fetch("/api/upload", {
100 | method: "POST",
101 | headers: {
102 | "Content-Type": "application/json",
103 | },
104 | body: body,
105 | });
106 | const res = await req.json();
107 | setComplete(true);
108 | router.push(`/snip/${res.slug}`);
109 | } catch (error) {
110 | console.log(error);
111 | setLoading(false);
112 | return error;
113 | }
114 | }
115 |
116 | useEffect(() => {
117 | const load = async () => {
118 | sdk.actions.ready();
119 | sdk.actions.addFrame()
120 | };
121 | if (sdk && !isSDKLoaded) {
122 | setIsSDKLoaded(true);
123 | load();
124 | }
125 | }, [isSDKLoaded]);
126 |
127 |
128 |
129 | function ButtonLoading() {
130 | return (
131 |
135 | );
136 | }
137 | function ButtonComplete() {
138 | return (
139 |
143 | );
144 | }
145 |
146 | return (
147 |
148 |
149 |
150 | ) =>
154 | setName(e.target.value)
155 | }
156 | />
157 |
176 |
177 |
191 |
192 | {loading && !complete && ButtonLoading()}
193 | {!loading && !complete && (
194 | <>
195 |
196 |
212 |
213 |
214 | setPassword(e.target.value)}
218 | className="w-full font-commitMono h-8 text-xs pr-10" // Added pr-10 for padding on the right
219 | />
220 |
231 |
232 |
233 |
234 | >
235 | )}
236 | {loading && complete && ButtonComplete()}
237 |
238 | );
239 | }
240 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export function Footer() {
4 | return (
5 |
6 |
Powered by
7 |
8 |
9 |
20 |
21 | +
22 |
23 |

24 |
25 |
26 |
27 | Built by{" "}
28 |
33 | Steve
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogHeader,
8 | DialogTitle,
9 | DialogTrigger,
10 | } from "./ui/dialog";
11 | import { Button } from "./ui/button";
12 | import { InfoCircledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
13 | import { Separator } from "./ui/separator";
14 | import Image from "next/image";
15 |
16 | export function Header() {
17 | return (
18 |
19 |
151 |
152 | );
153 | }
154 |
--------------------------------------------------------------------------------
/components/password-check.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { Button } from "./ui/button";
5 | import { Input } from "./ui/input";
6 | import { Card } from "./ui/card";
7 | import { EyeOpenIcon, EyeClosedIcon } from "@radix-ui/react-icons";
8 |
9 | interface PasswordCheckProps {
10 | onPasswordVerified: (content: string) => void;
11 | passwordHash: string;
12 | cid: string;
13 | }
14 |
15 | export function PasswordCheck({
16 | onPasswordVerified,
17 | passwordHash,
18 | cid,
19 | }: PasswordCheckProps) {
20 | const [password, setPassword] = useState("");
21 | const [error, setError] = useState(false);
22 | const [isVerifying, setIsVerifying] = useState(false);
23 | const [showPassword, setShowPassword] = useState(false);
24 |
25 | async function checkPassword() {
26 | try {
27 | setIsVerifying(true);
28 | setError(false);
29 |
30 | const response = await fetch(`/api/content/${cid}`, {
31 | method: "POST",
32 | headers: {
33 | "Content-Type": "application/json",
34 | },
35 | body: JSON.stringify({
36 | password: password,
37 | }),
38 | });
39 | const data = await response.json();
40 |
41 | if (response.ok && data.content) {
42 | onPasswordVerified(data.content);
43 | } else {
44 | setError(true);
45 | }
46 | } catch (err) {
47 | console.error("Password verification failed:", err);
48 | setError(true);
49 | } finally {
50 | setIsVerifying(false);
51 | }
52 | }
53 |
54 | return (
55 |
56 |
57 | This snip is password protected
58 |
59 |
60 |
61 | setPassword(e.target.value)}
65 | className={`w-full font-commitMono h-8 text-xs pr-10 ${error ? "border-red-500" : ""}`}
66 | onKeyDown={(e) => {
67 | if (e.key === "Enter") {
68 | checkPassword();
69 | }
70 | }}
71 | />
72 |
83 |
84 | {error &&
Incorrect password
}
85 |
92 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/components/password-content.tsx:
--------------------------------------------------------------------------------
1 | // components/protected-content.tsx
2 | "use client";
3 |
4 | import { useState } from "react";
5 | import { ReadOnlyEditor } from "./read-only-editor";
6 | import { PasswordCheck } from "./password-check";
7 | import type { LanguageName } from "@uiw/codemirror-extensions-langs";
8 |
9 | interface SnippetData {
10 | content: string;
11 | name: string;
12 | lang: LanguageName; // Import from @uiw/codemirror-extensions-langs
13 | expires: string;
14 | date: string;
15 | passwordHash: string;
16 | }
17 |
18 | interface ProtectedContentProps {
19 | data: SnippetData;
20 | cid: string;
21 | futureDate?: Date;
22 | slug?: string;
23 | }
24 |
25 | export function ProtectedContent({
26 | data,
27 | cid,
28 | futureDate,
29 | slug,
30 | }: ProtectedContentProps) {
31 | const [content, setContent] = useState(data.content);
32 | const [isVerified, setIsVerified] = useState(!data.passwordHash);
33 |
34 | async function handlePasswordVerified(verifiedContent: string) {
35 | setContent(verifiedContent);
36 | setIsVerified(true);
37 | }
38 |
39 | if (!isVerified) {
40 | return (
41 |
46 | );
47 | }
48 |
49 | return (
50 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/components/read-only-editor.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Card } from "@/components/ui/card";
4 | import { useEffect, useMemo, useState } from "react";
5 | import CodeMirror from "@uiw/react-codemirror";
6 | import { githubLight } from "@uiw/codemirror-theme-github";
7 | import { Button } from "./ui/button";
8 | import {
9 | CheckIcon,
10 | CopyIcon,
11 | DownloadIcon,
12 | Share1Icon,
13 | } from "@radix-ui/react-icons";
14 | import { Dialog, DialogTrigger } from "./ui/dialog";
15 | import { ShareModal } from "./share-modal";
16 | import { loadLanguage } from "@uiw/codemirror-extensions-langs";
17 | import type { LanguageName } from "@uiw/codemirror-extensions-langs";
18 | import sdk from '@farcaster/frame-sdk';
19 |
20 | type ReadOnlyEditorProps = {
21 | content: string;
22 | name: string;
23 | cid: string;
24 | lang: LanguageName;
25 | futureDate?: Date;
26 | slug?: string;
27 | };
28 |
29 | export function ReadOnlyEditor({
30 | content,
31 | name,
32 | cid,
33 | lang,
34 | futureDate,
35 | slug,
36 | }: ReadOnlyEditorProps) {
37 | const [copied, setCopied] = useState(false);
38 | const [isSDKLoaded, setIsSDKLoaded] = useState(false);
39 |
40 |
41 | let fileSlug = slug;
42 | if (slug === "") {
43 | fileSlug = cid;
44 | }
45 |
46 | const wait = () => new Promise((resolve) => setTimeout(resolve, 1000));
47 |
48 | const languageExtension = useMemo(() => {
49 | const extension = loadLanguage(lang);
50 | return extension ? [extension] : [];
51 | }, [lang]);
52 |
53 | async function handleCopy() {
54 | setCopied(true);
55 | await wait();
56 | setCopied(false);
57 | }
58 |
59 | async function copyToClipboard() {
60 | try {
61 | // Check if we have permission to use clipboard
62 | if (navigator.clipboard && navigator.permissions) {
63 | const permissionStatus = await navigator.permissions.query({ name: 'clipboard-write' as PermissionName });
64 |
65 | if (permissionStatus.state === 'granted' || permissionStatus.state === 'prompt') {
66 | await navigator.clipboard.writeText(content);
67 | await handleCopy();
68 | return;
69 | }
70 | }
71 |
72 | // Fallback method using execCommand (deprecated but works in more contexts)
73 | const textArea = document.createElement('textarea');
74 | textArea.value = content;
75 | textArea.style.position = 'fixed'; // Avoid scrolling to bottom
76 | document.body.appendChild(textArea);
77 | textArea.focus();
78 | textArea.select();
79 |
80 | const successful = document.execCommand('copy');
81 | document.body.removeChild(textArea);
82 |
83 | if (successful) {
84 | await handleCopy();
85 | } else {
86 | alert("Failed to copy: Your browser may be blocking clipboard access");
87 | }
88 | } catch (err) {
89 | console.error("Copy failed:", err);
90 | alert("Failed to copy: " + (err instanceof Error ? err.message : String(err)));
91 | }
92 | }
93 |
94 | function downloadContent() {
95 | const blob = new Blob([content], { type: "text/plain" });
96 | const url = URL.createObjectURL(blob);
97 | const a = document.createElement("a");
98 | a.href = url;
99 | a.download = name;
100 | document.body.appendChild(a);
101 | a.click();
102 | document.body.removeChild(a);
103 | URL.revokeObjectURL(url);
104 | }
105 |
106 | useEffect(() => {
107 | const load = async () => {
108 | sdk.actions.ready();
109 | sdk.actions.addFrame()
110 | };
111 | if (sdk && !isSDKLoaded) {
112 | setIsSDKLoaded(true);
113 | load();
114 | }
115 | }, [isSDKLoaded]);
116 |
117 | return (
118 |
119 |
173 |
174 | );
175 | }
176 |
--------------------------------------------------------------------------------
/components/share-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
4 |
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | DialogClose,
8 | DialogContent,
9 | DialogDescription,
10 | DialogFooter,
11 | DialogHeader,
12 | DialogTitle,
13 | } from "@/components/ui/dialog";
14 | import { Input } from "@/components/ui/input";
15 | import { Label } from "@/components/ui/label";
16 | import { useState } from "react";
17 |
18 | interface ShareModalProps {
19 | url: string;
20 | }
21 |
22 | export function ShareModal({ url }: ShareModalProps) {
23 | const [copied, setCopied] = useState(false);
24 |
25 | const wait = () => new Promise((resolve) => setTimeout(resolve, 1000));
26 |
27 | async function handleCopy() {
28 | setCopied(true);
29 | await wait();
30 | setCopied(false);
31 | }
32 |
33 | async function copyToClipboard() {
34 | navigator.clipboard
35 | .writeText(url)
36 | .then(async () => await handleCopy())
37 | .catch(() => alert("Failed to copy"));
38 | }
39 |
40 | return (
41 |
42 |
43 | Share link
44 |
45 | Anyone who has this link will be able to view this.
46 |
47 |
48 |
49 |
50 |
53 |
54 |
55 |
68 |
69 |
70 |
71 |
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | },
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | },
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = "CardTitle";
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = "CardDescription";
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = "CardContent";
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = "CardFooter";
78 |
79 | export {
80 | Card,
81 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5 | import { Check } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { X } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = "DialogHeader";
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = "DialogFooter";
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as LabelPrimitive from "@radix-ui/react-label";
3 | import { Slot } from "@radix-ui/react-slot";
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form";
12 |
13 | import { cn } from "@/lib/utils";
14 | import { Label } from "@/components/ui/label";
15 |
16 | const Form = FormProvider;
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath,
21 | > = {
22 | name: TName;
23 | };
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue,
27 | );
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath,
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext);
44 | const itemContext = React.useContext(FormItemContext);
45 | const { getFieldState, formState } = useFormContext();
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState);
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ");
51 | }
52 |
53 | const { id } = itemContext;
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | };
63 | };
64 |
65 | type FormItemContextValue = {
66 | id: string;
67 | };
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue,
71 | );
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId();
78 |
79 | return (
80 |
81 |
82 |
83 | );
84 | });
85 | FormItem.displayName = "FormItem";
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField();
92 |
93 | return (
94 |
100 | );
101 | });
102 | FormLabel.displayName = "FormLabel";
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } =
109 | useFormField();
110 |
111 | return (
112 |
123 | );
124 | });
125 | FormControl.displayName = "FormControl";
126 |
127 | const FormDescription = React.forwardRef<
128 | HTMLParagraphElement,
129 | React.HTMLAttributes
130 | >(({ className, ...props }, ref) => {
131 | const { formDescriptionId } = useFormField();
132 |
133 | return (
134 |
140 | );
141 | });
142 | FormDescription.displayName = "FormDescription";
143 |
144 | const FormMessage = React.forwardRef<
145 | HTMLParagraphElement,
146 | React.HTMLAttributes
147 | >(({ className, children, ...props }, ref) => {
148 | const { error, formMessageId } = useFormField();
149 | const body = error ? String(error?.message) : children;
150 |
151 | if (!body) {
152 | return null;
153 | }
154 |
155 | return (
156 |
162 | {body}
163 |
164 | );
165 | });
166 | FormMessage.displayName = "FormMessage";
167 |
168 | export {
169 | useFormField,
170 | Form,
171 | FormItem,
172 | FormLabel,
173 | FormControl,
174 | FormDescription,
175 | FormMessage,
176 | FormField,
177 | };
178 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SelectPrimitive from "@radix-ui/react-select";
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Select = SelectPrimitive.Root;
10 |
11 | const SelectGroup = SelectPrimitive.Group;
12 |
13 | const SelectValue = SelectPrimitive.Value;
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className,
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ));
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ));
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ));
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName;
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ));
100 | SelectContent.displayName = SelectPrimitive.Content.displayName;
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ));
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ));
135 | SelectItem.displayName = SelectPrimitive.Item.displayName;
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ));
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | };
161 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref,
15 | ) => (
16 |
27 | ),
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/lib/default.ts:
--------------------------------------------------------------------------------
1 | export const defaultCode: string =
2 | 'import { PinataSDK } from "pinata";\n' +
3 | "\n" +
4 | "const pinata = new PinataSDK({\n" +
5 | " pinataJwt: process.env.PINATA_JWT!,\n" +
6 | ' pinataGateway: "snippets.mypinata.cloud",\n' +
7 | "});\n" +
8 | "\n" +
9 | "async function upload() {\n" +
10 | " try {\n" +
11 | ' const file = new File(["Sharing code with Pinata"], "snippet.txt", { type: "text/plain" });\n' +
12 | " const data = await pinata.upload.file(file);\n" +
13 | " console.log(data);\n" +
14 | " } catch (error) {\n" +
15 | " console.log(error);\n" +
16 | " }\n" +
17 | "}\n" +
18 | "\n" +
19 | "upload();\n";
20 |
--------------------------------------------------------------------------------
/lib/languages.ts:
--------------------------------------------------------------------------------
1 | import type { LanguageName } from "@uiw/codemirror-extensions-langs";
2 |
3 | export const languages: {
4 | displayName: string;
5 | value: LanguageName;
6 | }[] = [
7 | { displayName: "Bash", value: "shell" },
8 | { displayName: "C", value: "c" },
9 | { displayName: "C#", value: "csharp" },
10 | { displayName: "CSS", value: "css" },
11 | { displayName: "Docker", value: "dockerfile" },
12 | { displayName: "Elm", value: "elm" },
13 | { displayName: "Erlang", value: "erlang" },
14 | { displayName: "Go", value: "go" },
15 | { displayName: "Haskell", value: "haskell" },
16 | { displayName: "HTML", value: "html" },
17 | { displayName: "Java", value: "java" },
18 | { displayName: "JavaScript", value: "javascript" },
19 | { displayName: "JSON", value: "json" },
20 | { displayName: "JSX", value: "jsx" },
21 | { displayName: "Kotlin", value: "kotlin" },
22 | { displayName: "Lua", value: "lua" },
23 | { displayName: "Markdown", value: "markdown" },
24 | { displayName: "Powershell", value: "powershell" },
25 | { displayName: "PHP", value: "php" },
26 | { displayName: "Python", value: "python" },
27 | { displayName: "R", value: "r" },
28 | { displayName: "Ruby", value: "ruby" },
29 | { displayName: "Rust", value: "rust" },
30 | { displayName: "Scala", value: "scala" },
31 | { displayName: "Solidity", value: "solidity" },
32 | { displayName: "SQL", value: "sql" },
33 | { displayName: "Swift", value: "swift" },
34 | { displayName: "Svelte", value: "svelte" },
35 | { displayName: "TOML", value: "toml" },
36 | { displayName: "TypeScript", value: "typescript" },
37 | { displayName: "TSX", value: "tsx" },
38 | { displayName: "Vue", value: "vue" },
39 | { displayName: "XML", value: "xml" },
40 | { displayName: "YAML", value: "yaml" },
41 | ];
42 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 |
4 | };
5 |
6 | export default nextConfig;
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "snippets",
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 | "@codemirror/lang-javascript": "^6.2.2",
13 | "@farcaster/frame-sdk": "^0.0.29",
14 | "@hookform/resolvers": "^3.6.0",
15 | "@radix-ui/react-checkbox": "^1.1.1",
16 | "@radix-ui/react-dialog": "^1.1.1",
17 | "@radix-ui/react-icons": "^1.3.0",
18 | "@radix-ui/react-label": "^2.1.0",
19 | "@radix-ui/react-select": "^2.1.2",
20 | "@radix-ui/react-separator": "^1.1.0",
21 | "@radix-ui/react-slot": "^1.1.0",
22 | "@uiw/codemirror-extensions-langs": "^4.23.0",
23 | "@uiw/codemirror-theme-github": "^4.22.2",
24 | "@uiw/react-codemirror": "^4.22.2",
25 | "@vercel/analytics": "^1.3.1",
26 | "argon2": "0.31.2",
27 | "base64-loader": "^1.0.0",
28 | "class-variance-authority": "^0.7.0",
29 | "clsx": "^2.1.1",
30 | "lucide-react": "^0.399.0",
31 | "nanoid": "^5.0.8",
32 | "next": "^14.2.24",
33 | "pinata": "^1.7.2",
34 | "react": "^18",
35 | "react-dom": "^18",
36 | "react-hook-form": "^7.52.0",
37 | "tailwind-merge": "^2.3.0",
38 | "tailwindcss-animate": "^1.0.7",
39 | "zod": "^3.23.8"
40 | },
41 | "devDependencies": {
42 | "@types/argon2-browser": "^1.18.4",
43 | "@types/node": "^20",
44 | "@types/react": "^18",
45 | "@types/react-dom": "^18",
46 | "postcss": "^8",
47 | "tailwindcss": "^3.4.1",
48 | "typescript": "^5"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/CommitMono-400-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/CommitMono-400-Regular.otf
--------------------------------------------------------------------------------
/public/CommitMono-700-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/CommitMono-700-Regular.otf
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/bmc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/bmc.png
--------------------------------------------------------------------------------
/public/ethereum.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/icon.png
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/og.png
--------------------------------------------------------------------------------
/public/pinata.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevedylandev/snippets/1a3768ae7edf198c438759e701320085f4765d93/public/pinata.png
--------------------------------------------------------------------------------
/public/script.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | GITHUB_REPO="https://github.com/stevedylandev/snippets-cli"
5 | CLI_NAME="snip"
6 |
7 | INSTALL_DIR="$HOME/.local/share"
8 | BIN_DIR="$INSTALL_DIR/snippets"
9 |
10 | # Color codes for output
11 | RED='\033[0;31m'
12 | GREEN='\033[0;32m'
13 | NC='\033[0m' # No Color
14 |
15 | error() {
16 | echo -e "${RED}error:${NC} $*" >&2
17 | exit 1
18 | }
19 |
20 | success() {
21 | echo -e "${GREEN}$*${NC}"
22 | }
23 |
24 | # Detect platform
25 | detect_platform() {
26 | local platform=$(uname -s)
27 | local arch=$(uname -m)
28 |
29 | case "$platform" in
30 | "Darwin")
31 | platform="Darwin"
32 | ;;
33 | "Linux")
34 | platform="Linux"
35 | ;;
36 | MINGW*|MSYS*|CYGWIN*)
37 | platform="Windows"
38 | ;;
39 | *)
40 | error "Unsupported platform: $platform"
41 | ;;
42 | esac
43 |
44 | case "$arch" in
45 | "x86_64"|"amd64")
46 | arch="x86_64"
47 | ;;
48 | "arm64"|"aarch64")
49 | arch="arm64"
50 | ;;
51 | "i386"|"i686")
52 | arch="i386"
53 | ;;
54 | *)
55 | error "Unsupported architecture: $arch"
56 | ;;
57 | esac
58 |
59 | echo "${platform}_${arch}"
60 | }
61 |
62 | # Download and install the CLI
63 | install_cli() {
64 | local platform=$1
65 | local download_url="${GITHUB_REPO}/releases/latest/download/snippets-cli_${platform}.tar.gz"
66 | local temp_dir=$(mktemp -d)
67 |
68 | echo "Downloading ${CLI_NAME}..."
69 | curl -L "$download_url" -o "$temp_dir/${CLI_NAME}.tar.gz" || error "Failed to download ${CLI_NAME}"
70 |
71 | echo "Extracting ${CLI_NAME}..."
72 | tar -xzf "$temp_dir/${CLI_NAME}.tar.gz" -C "$temp_dir" || error "Failed to extract ${CLI_NAME}"
73 |
74 | mkdir -p "$BIN_DIR" || error "Failed to create bin directory"
75 | mv "$temp_dir/${CLI_NAME}" "$BIN_DIR/" || error "Failed to move ${CLI_NAME} to bin directory"
76 | chmod +x "$BIN_DIR/${CLI_NAME}" || error "Failed to make ${CLI_NAME} executable"
77 |
78 | rm -rf "$temp_dir"
79 | }
80 |
81 | # Update shell configuration
82 | update_shell_config() {
83 | local shell_config
84 | case $SHELL in
85 | */zsh)
86 | shell_config="$HOME/.zshrc"
87 | ;;
88 | */bash)
89 | shell_config="$HOME/.bashrc"
90 | ;;
91 | */fish)
92 | shell_config="$HOME/.config/fish/config.fish"
93 | ;;
94 | *)
95 | echo "Unsupported shell. Please add the following to your shell configuration:"
96 | echo "export PATH=\"$BIN_DIR:\$PATH\""
97 | return
98 | ;;
99 | esac
100 |
101 | echo "Updating shell configuration..."
102 | echo "export PATH=\"$BIN_DIR:\$PATH\"" >> "$shell_config"
103 | echo "Shell configuration updated. Please restart your shell or run 'source $shell_config'"
104 | }
105 |
106 | main() {
107 | local platform=$(detect_platform)
108 | install_cli "$platform"
109 | update_shell_config
110 | success "${CLI_NAME} has been successfully installed to $BIN_DIR/${CLI_NAME}"
111 | echo "Run '${CLI_NAME} --help' to get started"
112 | }
113 |
114 | main
115 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/public/vercel-icon-dark.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{ts,tsx}",
7 | "./components/**/*.{ts,tsx}",
8 | "./app/**/*.{ts,tsx}",
9 | "./src/**/*.{ts,tsx}",
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | fontFamily: {
22 | commitMono: ["var(--font-commitMono)"],
23 | },
24 | colors: {
25 | border: "hsl(var(--border))",
26 | input: "hsl(var(--input))",
27 | ring: "hsl(var(--ring))",
28 | background: "hsl(var(--background))",
29 | foreground: "hsl(var(--foreground))",
30 | primary: {
31 | DEFAULT: "hsl(var(--primary))",
32 | foreground: "hsl(var(--primary-foreground))",
33 | },
34 | secondary: {
35 | DEFAULT: "hsl(var(--secondary))",
36 | foreground: "hsl(var(--secondary-foreground))",
37 | },
38 | destructive: {
39 | DEFAULT: "hsl(var(--destructive))",
40 | foreground: "hsl(var(--destructive-foreground))",
41 | },
42 | muted: {
43 | DEFAULT: "hsl(var(--muted))",
44 | foreground: "hsl(var(--muted-foreground))",
45 | },
46 | accent: {
47 | DEFAULT: "hsl(var(--accent))",
48 | foreground: "hsl(var(--accent-foreground))",
49 | },
50 | popover: {
51 | DEFAULT: "hsl(var(--popover))",
52 | foreground: "hsl(var(--popover-foreground))",
53 | },
54 | card: {
55 | DEFAULT: "hsl(var(--card))",
56 | foreground: "hsl(var(--card-foreground))",
57 | },
58 | },
59 | borderRadius: {
60 | lg: "var(--radius)",
61 | md: "calc(var(--radius) - 2px)",
62 | sm: "calc(var(--radius) - 4px)",
63 | },
64 | keyframes: {
65 | "accordion-down": {
66 | from: { height: "0" },
67 | to: { height: "var(--radix-accordion-content-height)" },
68 | },
69 | "accordion-up": {
70 | from: { height: "var(--radix-accordion-content-height)" },
71 | to: { height: "0" },
72 | },
73 | },
74 | animation: {
75 | "accordion-down": "accordion-down 0.2s ease-out",
76 | "accordion-up": "accordion-up 0.2s ease-out",
77 | },
78 | },
79 | },
80 | plugins: [require("tailwindcss-animate")],
81 | } satisfies Config;
82 |
83 | export default config;
84 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------