├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── README.md ├── components ├── home │ ├── card.tsx │ ├── component-grid.tsx │ └── terminal.tsx ├── layout │ ├── index.tsx │ └── meta.tsx └── shared │ ├── carbonAds.tsx │ ├── counting-numbers.tsx │ ├── googleAnalytics.tsx │ ├── icons │ ├── copy.tsx │ ├── expanding-arrow.tsx │ ├── github.tsx │ ├── index.tsx │ ├── keyboard-icon.tsx │ ├── loading-circle.tsx │ ├── loading-dots.module.css │ ├── loading-dots.tsx │ ├── loading-spinner.module.css │ ├── loading-spinner.tsx │ └── prompt.tsx │ ├── leaflet.tsx │ ├── modal.tsx │ ├── popover.tsx │ └── tooltip.tsx ├── lib ├── hooks │ ├── use-intersection-observer.ts │ ├── use-local-storage.ts │ ├── use-scroll.ts │ └── use-window-size.ts └── utils.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ ├── generate.ts │ └── og.tsx ├── index.tsx └── privacy.tsx ├── postcss.config.js ├── prettier.config.js ├── public ├── authjs.webp ├── bg-pattern.svg ├── favicon.ico ├── logo-white.png ├── logo.png └── logo.svg ├── styles ├── SF-Pro-Display-Medium.otf └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── utils └── OpenAIStream.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | NEXT_PUBLIC_GA_ID= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # IDEs 40 | .vscode 41 | **/.idea 42 | **/coverage 43 | *.sublime-project 44 | *.sublime-workspace 45 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | node_modules 3 | .next 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | GitFluence – Generate the git commands using ChatGPT 3 |

GitFluence

4 |
5 | 6 |

7 | Generate the git commands using ChatGPT 8 |

9 | 10 |

11 | Introduction · 12 | Tech Stack + Features · 13 |

14 |
15 | 16 | ## Introduction 17 | 18 | GitFluence is an AI-driven solution that helps you quickly find the right command. Get started with Git Command Generator today and save time. 19 | 20 | ## Tech Stack + Features 21 | 22 | ### Frameworks 23 | 24 | - [Next.js](https://nextjs.org/) – React framework for building performant apps with the best developer experience 25 | 26 | ### Platforms 27 | 28 | - [Vercel](https://vercel.com/) – Easily preview & deploy changes with git 29 | 30 | ### UI 31 | 32 | - [Tailwind CSS](https://tailwindcss.com/) – Utility-first CSS framework for rapid UI development 33 | - [Radix](https://www.radix-ui.com/) – Primitives like modal, popover, etc. to build a stellar user experience 34 | - [Lucide](https://lucide.dev/) – Beautifully simple, pixel-perfect icons 35 | - [`@next/font`](https://nextjs.org/docs/basic-features/font-optimization) – Optimize custom fonts and remove external network requests for improved performance 36 | - [`@vercel/og`](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) – Generate dynamic Open Graph images on the edge 37 | - [`react-wrap-balancer`](https://github.com/shuding/react-wrap-balancer) – Simple React component that makes titles more readable 38 | 39 | ### Code Quality 40 | 41 | - [TypeScript](https://www.typescriptlang.org/) – Static type checker for end-to-end typesafety 42 | - [Prettier](https://prettier.io/) – Opinionated code formatter for consistent code style 43 | - [ESLint](https://eslint.org/) – Pluggable linter for Next.js and TypeScript 44 | 45 | ### Miscellaneous 46 | 47 | - [Vercel Analytics](https://vercel.com/analytics) – Track unique visitors, pageviews, and more in a privacy-friendly way 48 | -------------------------------------------------------------------------------- /components/home/card.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import ReactMarkdown from "react-markdown"; 3 | import Balancer from "react-wrap-balancer"; 4 | 5 | export default function Card({ 6 | title, 7 | description, 8 | demo, 9 | large, 10 | }: { 11 | title: string; 12 | description: string; 13 | demo: ReactNode; 14 | large?: boolean; 15 | }) { 16 | return ( 17 |
22 |
{demo}
23 |
24 |

25 | {title} 26 |

27 |
28 | 29 | ( 32 | 38 | ), 39 | code: ({ node, ...props }) => ( 40 | 46 | ), 47 | }} 48 | > 49 | {description} 50 | 51 | 52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /components/home/component-grid.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Popover from "@/components/shared/popover"; 3 | import Tooltip from "@/components/shared/tooltip"; 4 | import { ChevronDown } from "lucide-react"; 5 | 6 | export default function ComponentGrid() { 7 | const [openPopover, setOpenPopover] = useState(false); 8 | return ( 9 |
10 | 13 | 16 | 19 | 22 | 25 |
26 | } 27 | openPopover={openPopover} 28 | setOpenPopover={setOpenPopover} 29 | > 30 | 41 | 42 | 43 |
44 |

Tooltip

45 |
46 |
47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /components/home/terminal.tsx: -------------------------------------------------------------------------------- 1 | import { Copy } from "@/components/shared/icons"; 2 | import classNames from "classnames"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import { toast } from "react-hot-toast"; 5 | 6 | interface Response { 7 | type: string; 8 | content: string; 9 | } 10 | 11 | const responseType = { 12 | answer: "answer", 13 | question: "question", 14 | }; 15 | 16 | function removeMarkdown(text: string) { 17 | text = text.replace(/`(.+?)`/g, ""); 18 | 19 | return text; 20 | } 21 | 22 | export default function Terminal() { 23 | const [input, setInput] = useState(""); 24 | const [loading, setLoading] = useState(false); 25 | const [responses, setResponses] = useState([]); 26 | const inputRef = useRef(null); 27 | 28 | useEffect(() => { 29 | setInputFocus(); 30 | }, []); 31 | 32 | const setInputFocus = () => { 33 | if (inputRef.current) { 34 | inputRef.current.focus(); 35 | } 36 | }; 37 | 38 | const generateCommand = async () => { 39 | setLoading(true); 40 | setResponses((prev) => { 41 | return [ 42 | ...prev, 43 | { 44 | type: responseType.question, 45 | content: input, 46 | }, 47 | ]; 48 | }); 49 | setInput(""); 50 | 51 | const response = await fetch("/api/generate", { 52 | method: "POST", 53 | headers: { 54 | "Content-Type": "application/json", 55 | }, 56 | body: JSON.stringify({ 57 | prompt: input, 58 | }), 59 | }); 60 | 61 | // This data is a ReadableStream 62 | const data = response.body; 63 | if (!data) { 64 | return; 65 | } 66 | 67 | const reader = data.getReader(); 68 | const decoder = new TextDecoder(); 69 | let done = false; 70 | let resultData = ""; 71 | 72 | while (!done) { 73 | const { value, done: doneReading } = await reader.read(); 74 | done = doneReading; 75 | const chunkValue = decoder.decode(value); 76 | resultData += chunkValue; 77 | } 78 | 79 | setResponses((prev) => { 80 | return [ 81 | ...prev, 82 | { 83 | type: responseType.answer, 84 | content: removeMarkdown(resultData), 85 | }, 86 | ]; 87 | }); 88 | 89 | setLoading(false); 90 | setTimeout(() => { 91 | setInputFocus(); 92 | 93 | if (typeof (window as any)._carbonads !== "undefined") { 94 | (window as any)._carbonads.refresh(); 95 | } 96 | }, 250); 97 | }; 98 | 99 | return ( 100 |
101 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | 114 |
115 | {responses.map((item, index) => { 116 | return ( 117 |
118 | {item.type === responseType.question && ( 119 |
120 | {"> "} 121 | {item.content} 122 |
123 | )} 124 | 125 | {loading && index === responses.length - 1 ? ( 126 | 127 | 128 | \ 129 | | 130 | / 131 | - 132 | 133 | 134 | ) : item.type === responseType.answer ? ( 135 |
136 | {"> "} 137 | {item.content} 138 | 139 | {item.content.length > 0 && 140 | !item.content.includes("💬") && 141 | !item.content.includes("🚨") && ( 142 | 183 | )} 184 |
185 | ) : null} 186 |
187 | ); 188 | })} 189 | 190 | {!loading && ( 191 |
192 |
{">"}
193 |
{ 196 | e.preventDefault(); 197 | generateCommand(); 198 | }} 199 | className="w-full" 200 | > 201 | setInput(e.target.value)} 209 | /> 210 |
211 |
212 | )} 213 |
214 |
215 | 216 |
217 |
218 | ); 219 | } 220 | -------------------------------------------------------------------------------- /components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React, { useState } from "react"; 3 | import { ReactNode, useEffect } from "react"; 4 | import Meta from "./meta"; 5 | import { Github } from "@/components/shared/icons"; 6 | 7 | export default function Layout({ 8 | meta, 9 | children, 10 | }: { 11 | meta?: { 12 | title?: string; 13 | description?: string; 14 | image?: string; 15 | }; 16 | children: ReactNode; 17 | }) { 18 | const [terminalRef, setTerminalRef] = useState(null); 19 | 20 | useEffect(() => { 21 | if (document) { 22 | setTerminalRef(document.getElementById("terminal")); 23 | } 24 | }, []); 25 | 26 | const scrollToTerminal = () => { 27 | if (terminalRef !== null) { 28 | terminalRef.scrollIntoView({ behavior: "smooth" }); 29 | } 30 | }; 31 | 32 | return ( 33 | <> 34 | 35 | 36 |
37 |
38 |
39 | 43 |

GitFluence

44 | 45 | 46 |
47 | 52 |
53 |
54 |
55 | 56 |
57 | {children} 58 |
59 | 60 |
61 |
62 | 63 |
88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /components/layout/meta.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | const DOMAIN = "https://gitfluence.com"; 4 | 5 | export default function Meta({ 6 | title = "GitFluence - Find the Git Command You Need Now!", 7 | description = "GitFluence is AI-driven solution that helps you quickly find the right command. Get started with Git Command Generator today and save time.", 8 | image = `${DOMAIN}/api/og`, 9 | }: { 10 | title?: string; 11 | description?: string; 12 | image?: string; 13 | }) { 14 | return ( 15 | 16 | {title} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /components/shared/carbonAds.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | import { useEffect, useRef } from "react"; 3 | 4 | const carbonStyle = ` 5 | #carbonads { 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 7 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, 8 | sans-serif; 9 | } 10 | 11 | #carbonads { 12 | display: block; 13 | overflow: hidden; 14 | max-width: 728px; 15 | position: relative; 16 | background-color: hsl(0, 0%, 12%); 17 | border: solid 1px hsl(0, 0%, 15%); 18 | font-size: 22px; 19 | box-sizing: content-box; 20 | color: hsl(0, 0%, 90%); 21 | } 22 | 23 | #carbonads > span { 24 | display: block; 25 | } 26 | 27 | #carbonads a { 28 | color: inherit; 29 | text-decoration: none; 30 | } 31 | 32 | #carbonads a:hover { 33 | color: inherit; 34 | } 35 | 36 | .carbon-wrap { 37 | display: flex; 38 | align-items: center; 39 | } 40 | 41 | .carbon-img { 42 | display: block; 43 | margin: 0; 44 | line-height: 1; 45 | } 46 | 47 | .carbon-img img { 48 | display: block; 49 | height: 100px; 50 | width: auto; 51 | } 52 | 53 | .carbon-text { 54 | display: block; 55 | padding: 0 1em; 56 | line-height: 1.35; 57 | text-align: left; 58 | } 59 | 60 | .carbon-poweredby { 61 | display: block; 62 | position: absolute; 63 | bottom: 0; 64 | right: 0; 65 | padding: 6px 10px; 66 | background: hsl(0, 0%, 15%); 67 | color: hsl(0, 0%, 90%); 68 | text-align: center; 69 | text-transform: uppercase; 70 | letter-spacing: 0.5px; 71 | font-weight: 600; 72 | font-size: 8px; 73 | border-top-left-radius: 4px; 74 | line-height: 1; 75 | } 76 | 77 | @media only screen and (min-width: 320px) and (max-width: 759px) { 78 | .carbon-text { 79 | font-size: 14px; 80 | } 81 | } 82 | `; 83 | 84 | export default function CarbonAds() { 85 | const dataFetch = useRef(false); 86 | const containerRef = useRef(null); 87 | 88 | useEffect(() => { 89 | if (dataFetch.current) return; 90 | dataFetch.current = true; 91 | 92 | const script = document.createElement("script"); 93 | script.id = "_carbonads_js"; 94 | script.src = `//cdn.carbonads.com/carbon.js?serve=CWYDC53J&placement=gitfluencecom'`; 95 | containerRef.current?.appendChild(script); 96 | }, []); 97 | 98 | return ( 99 | <> 100 | -------------------------------------------------------------------------------- /styles/SF-Pro-Display-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geovanesantana/gitfluence/52d4774ed71850450da931c010eb403fc04f0da3/styles/SF-Pro-Display-Medium.otf -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | background-color: #17181d; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const plugin = require("tailwindcss/plugin"); 3 | 4 | module.exports = { 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx}", 7 | "./components/**/*.{js,ts,jsx,tsx}", 8 | ], 9 | future: { 10 | hoverOnlyWhenSupported: true, 11 | }, 12 | theme: { 13 | extend: { 14 | spacing: { 15 | 100: "6.25rem", 16 | }, 17 | colors: { 18 | light: "#fffef1", 19 | gray: "#c1c0b4", 20 | black: "#222326", 21 | "black-400": "#343333", 22 | "black-500": "#1c1d22", 23 | "black-600": "#232021", 24 | "black-700": "#262321", 25 | slate: "#17181d", 26 | zinc: "#232323", 27 | amber: "#ffac64", 28 | "amber-400": "#FFB677", 29 | "amber-600": "#c18d35", 30 | "amber-700": "#A07731", 31 | yellow: "#f2b132", 32 | "yellow-400": "#F4C15F", 33 | }, 34 | fontFamily: { 35 | display: ["var(--font-sf)", "system-ui", "sans-serif"], 36 | default: ["var(--font-inter)", "system-ui", "sans-serif"], 37 | }, 38 | animation: { 39 | "slide-up-fade": "slide-up-fade 0.3s cubic-bezier(0.16, 1, 0.3, 1)", 40 | "slide-down-fade": "slide-down-fade 0.3s cubic-bezier(0.16, 1, 0.3, 1)", 41 | }, 42 | keyframes: { 43 | "fade-in": { 44 | from: { opacity: 0, transform: "translateY(-10px)" }, 45 | to: { opacity: 1, transform: "none" }, 46 | }, 47 | "slide-up-fade": { 48 | "0%": { opacity: 0, transform: "translateY(6px)" }, 49 | "100%": { opacity: 1, transform: "translateY(0)" }, 50 | }, 51 | "slide-down-fade": { 52 | "0%": { opacity: 0, transform: "translateY(-6px)" }, 53 | "100%": { opacity: 1, transform: "translateY(0)" }, 54 | }, 55 | tiles: { 56 | "0%": { 57 | transform: "translateX(0)", 58 | }, 59 | "100%": { 60 | transform: "translateX(-4em)", 61 | }, 62 | }, 63 | }, 64 | backgroundImage: { 65 | "hero-pattern": "url('/bg-pattern.svg')", 66 | "terminal-pattern": 67 | "linear-gradient(91.57deg, rgba(250, 190, 101, 0.5) -15.63%, rgba(190, 127, 107, 0.5) 16.07%, rgba(131, 155, 251, 0.5) 108.52%)", 68 | "radial-gradient": 69 | "radial-gradient(202.04% 202.04% at 50% 111.22%, rgba(255, 254, 241, 0.2) 0%, rgba(120, 120, 120, 0.2) 23.95%)", 70 | "feature-pattern": 71 | "linear-gradient(91.57deg, rgba(250, 190, 101, 0.7) -15.63%, rgba(190, 127, 107, 0.7) 35.95%, rgba(131, 155, 251, 0.7) 108.52%)", 72 | }, 73 | boxShadow: { 74 | "3xl": "0 0 60px 3px rgb(0 0 0 / 40%)", 75 | }, 76 | animation: { 77 | "fade-in": "fade-in 1000ms var(--animation-delay, 0ms) ease forwards", 78 | tiles: "tiles 600ms steps(4) infinite", 79 | }, 80 | }, 81 | }, 82 | plugins: [ 83 | require("@tailwindcss/forms"), 84 | require("@tailwindcss/typography"), 85 | require("@tailwindcss/line-clamp"), 86 | plugin(({ addVariant }) => { 87 | addVariant("radix-side-top", '&[data-side="top"]'); 88 | addVariant("radix-side-bottom", '&[data-side="bottom"]'); 89 | }), 90 | ], 91 | }; 92 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/components/*": ["components/*"], 10 | "@/pages/*": ["pages/*"], 11 | "@/lib/*": ["lib/*"], 12 | "@/styles/*": ["styles/*"] 13 | }, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noEmit": true, 17 | "esModuleInterop": true, 18 | "module": "esnext", 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "jsx": "preserve", 23 | "incremental": true 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /utils/OpenAIStream.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createParser, 3 | ParsedEvent, 4 | ReconnectInterval, 5 | } from "eventsource-parser"; 6 | 7 | export type ChatGPTAgent = "user" | "system"; 8 | 9 | export interface ChatGPTMessage { 10 | role: ChatGPTAgent; 11 | content: string; 12 | } 13 | 14 | export interface OpenAIStreamPayload { 15 | model: string; 16 | messages: ChatGPTMessage[]; 17 | temperature: number; 18 | top_p: number; 19 | frequency_penalty: number; 20 | presence_penalty: number; 21 | max_tokens: number; 22 | stream: boolean; 23 | n: number; 24 | } 25 | 26 | export async function OpenAIStream(payload: OpenAIStreamPayload) { 27 | const encoder = new TextEncoder(); 28 | const decoder = new TextDecoder(); 29 | 30 | let counter = 0; 31 | 32 | const res = await fetch("https://api.openai.com/v1/chat/completions", { 33 | headers: { 34 | "Content-Type": "application/json", 35 | Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ""}`, 36 | }, 37 | method: "POST", 38 | body: JSON.stringify(payload), 39 | }); 40 | 41 | const stream = new ReadableStream({ 42 | async start(controller) { 43 | // callback 44 | function onParse(event: ParsedEvent | ReconnectInterval) { 45 | if (event.type === "event") { 46 | const data = event.data; 47 | // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream 48 | if (data === "[DONE]") { 49 | controller.close(); 50 | return; 51 | } 52 | try { 53 | const json = JSON.parse(data); 54 | const text = json.choices[0].delta?.content || ""; 55 | if (counter < 2 && (text.match(/\n/) || []).length) { 56 | // this is a prefix character (i.e., "\n\n"), do nothing 57 | return; 58 | } 59 | const queue = encoder.encode(text); 60 | controller.enqueue(queue); 61 | counter++; 62 | } catch (e) { 63 | // maybe parse error 64 | controller.error(e); 65 | } 66 | } 67 | } 68 | 69 | // stream response (SSE) from OpenAI may be fragmented into multiple chunks 70 | // this ensures we properly read chunks and invoke an event for each SSE event stream 71 | const parser = createParser(onParse); 72 | // https://web.dev/streams/#asynchronous-iteration 73 | for await (const chunk of res.body as any) { 74 | parser.feed(decoder.decode(chunk)); 75 | } 76 | }, 77 | }); 78 | 79 | return stream; 80 | } 81 | --------------------------------------------------------------------------------