Last active
May 11, 2026 10:11
-
-
Save affandhia/b5df54b8d10699dfdc0fecabafaea5cb to your computer and use it in GitHub Desktop.
Portal for all gist standalone apps and related stuff.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <!-- Core document metadata --> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Gist App Portal</title> | |
| <meta | |
| name="description" | |
| content="A standalone Gist App Portal built with React, Tailwind CSS, Framer Motion, and Lucide React." | |
| /> | |
| <meta name="keywords" content="React, Tailwind CSS, Framer Motion, Lucide, Gist App Portal" /> | |
| <meta name="author" content="Your Name" /> | |
| <meta name="robots" content="index, follow" /> | |
| <!-- Canonical URL --> | |
| <link rel="canonical" href="https://example.com/" /> | |
| <!-- Inline SVG favicon: fully standalone, no external favicon file needed --> | |
| <link | |
| rel="icon" | |
| type="image/svg+xml" | |
| href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%230b1224'/%3E%3Cpath d='M18 39c7-15 21-15 28 0' fill='none' stroke='%233dd5f3' stroke-width='5' stroke-linecap='round'/%3E%3Ccircle cx='24' cy='25' r='4' fill='white'/%3E%3Ccircle cx='40' cy='25' r='4' fill='white'/%3E%3Cpath d='M22 46h20' stroke='white' stroke-width='4' stroke-linecap='round' opacity='.8'/%3E%3C/svg%3E" | |
| /> | |
| <!-- Theme and mobile metadata --> | |
| <meta name="theme-color" content="#0b1224" /> | |
| <meta name="color-scheme" content="dark light" /> | |
| <meta name="application-name" content="Gist App Portal" /> | |
| <meta name="apple-mobile-web-app-title" content="Gist App Portal" /> | |
| <meta name="apple-mobile-web-app-capable" content="yes" /> | |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> | |
| <!-- Open Graph metadata --> | |
| <meta property="og:type" content="website" /> | |
| <meta property="og:site_name" content="Gist App Portal" /> | |
| <meta property="og:title" content="Gist App Portal" /> | |
| <meta | |
| property="og:description" | |
| content="A standalone Gist App Portal built with React, Tailwind CSS, Framer Motion, and Lucide React." | |
| /> | |
| <meta property="og:url" content="https://example.com/" /> | |
| <meta | |
| property="og:image" | |
| content="https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&w=1200&h=630&q=80" | |
| /> | |
| <meta property="og:image:alt" content="Laptop showing code on a desk" /> | |
| <meta property="og:image:width" content="1200" /> | |
| <meta property="og:image:height" content="630" /> | |
| <meta property="og:locale" content="en_US" /> | |
| <!-- Twitter/X card metadata --> | |
| <meta name="twitter:card" content="summary_large_image" /> | |
| <meta name="twitter:title" content="Gist App Portal" /> | |
| <meta | |
| name="twitter:description" | |
| content="A standalone Gist App Portal built with React, Tailwind CSS, Framer Motion, and Lucide React." | |
| /> | |
| <meta | |
| name="twitter:image" | |
| content="https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&w=1200&h=630&q=80" | |
| /> | |
| <meta name="twitter:image:alt" content="Laptop showing code on a desk" /> | |
| <!-- Optional: set these if you have Twitter/X handles --> | |
| <meta name="twitter:site" content="@yourhandle" /> | |
| <meta name="twitter:creator" content="@yourhandle" /> | |
| <!-- Tailwind Play CDN: quick dev only, not for production --> | |
| <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> | |
| <!-- Babel standalone to compile JSX in-browser: dev only --> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <!-- Import map: all bare imports resolve from here --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "react": "https://esm.sh/react@18.2.0?dev", | |
| "react-dom": "https://esm.sh/react-dom@18.2.0?dev", | |
| "react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev", | |
| "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime?dev", | |
| "react/jsx-dev-runtime": "https://esm.sh/react@18.2.0/jsx-dev-runtime?dev", | |
| "framer-motion": "https://esm.sh/framer-motion@12.26.0?dev&external=react,react-dom", | |
| "lucide-react": "https://esm.sh/lucide-react@0.268.0?dev&external=react", | |
| "clsx": "https://esm.sh/clsx@2.1.0?dev", | |
| "tailwind-merge": "https://esm.sh/tailwind-merge@2.5.2?dev", | |
| "fuse.js": "https://esm.sh/fuse.js@7.0.0?dev" | |
| } | |
| } | |
| </script> | |
| <style type="text/tailwindcss"> | |
| @theme { | |
| --color-ink: #0b1224; | |
| --color-brand: #3dd5f3; | |
| --color-userscript: #a855f7; | |
| } | |
| @layer base { | |
| body { | |
| @apply antialiased; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen bg-ink text-white"> | |
| <div id="app"></div> | |
| <script type="text/babel" data-type="module" data-presets="react"> | |
| import React, { useState, useMemo, useRef, useEffect } from 'react'; | |
| import { createRoot } from 'react-dom/client'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { Search, ExternalLink, FileCode2, AppWindow, Github } from 'lucide-react'; | |
| import clsx from 'clsx'; | |
| import { twMerge } from 'tailwind-merge'; | |
| import Fuse from 'fuse.js'; | |
| const cn = (...args) => twMerge(clsx(args)); | |
| // Utility function to highlight text | |
| const highlightText = (text, matches, highlightClass) => { | |
| if (!matches || matches.length === 0) { | |
| return text; | |
| } | |
| const result = []; | |
| let lastIndex = 0; | |
| matches.forEach(match => { | |
| const [start, end] = match; // indices are inclusive start, exclusive end for Fuse.js | |
| // Add text before the match | |
| if (start > lastIndex) { | |
| result.push(text.substring(lastIndex, start)); | |
| } | |
| // Add the highlighted text | |
| result.push(<span className={highlightClass} key={start + '-' + end}>{text.substring(start, end + 1)}</span>); | |
| lastIndex = end + 1; | |
| }); | |
| // Add any remaining text after the last match | |
| if (lastIndex < text.length) { | |
| result.push(text.substring(lastIndex)); | |
| } | |
| return result; | |
| }; | |
| const gistData = [ | |
| { | |
| id: '1', | |
| title: 'Env File Merger', | |
| description: 'Merge multiple .env files and add overrides.', | |
| url: 'https://gist.githack.com/affanpg/1b9d60e16a0e1109ffb41db186dcafb8/raw/f694e9d787cc50edacb47d0d711c549879dc658b/env-merger.html', | |
| gistUrl: 'https://gist.github.com/affanpg/1b9d60e16a0e1109ffb41db186dcafb8' | |
| }, | |
| { | |
| id: '2', | |
| title: 'SMTP Copy Button Userscript', | |
| description: "Adds a 'Copy OTP' button to SMTP pages.", | |
| url: 'https://gist.githack.com/affanpg/9d822b707ce89848237b0a9736e681f9/raw/77665b361dcf900eae6b8ca10de0a6d2b61e053a/smtp-copy-button-script.user.js', | |
| gistUrl: 'https://gist.github.com/affanpg/9d822b707ce89848237b0a9736e681f9' | |
| }, | |
| { | |
| id: '3', | |
| title: 'GitHub Resolve All Conversations Userscript', | |
| description: 'Adds a button to resolve all conversations in a GitHub PR.', | |
| url: 'https://gist.githack.com/affanpg/97151fb0659214aa31776df063f3a710/raw/1d5930262af0022eaa8954427692457aff2de5e1/resolve-all-conversations.user.js', | |
| gistUrl: 'https://gist.github.com/affanpg/97151fb0659214aa31776df063f3a710' | |
| }, | |
| { | |
| id: '4', | |
| title: 'React + Tailwind + Framer Motion + Lucide standalone starter kit', | |
| description: 'A standalone starter kit for building web apps with React, Tailwind CSS, Framer Motion, and Lucide.', | |
| url: 'https://gist.githack.com/affanpg/0aac4c511edcb22b15571f3bb44a9ab4/raw/a6b139a0399554a4755913e6488a033f923b320e/react-tailwind-framer-lucide-starter.html', | |
| gistUrl: 'https://gist.github.com/affanpg/0aac4c511edcb22b15571f3bb44a9ab4' | |
| }, | |
| { | |
| id: '5', | |
| title: 'JSON Field Picker', | |
| description: 'An interactive tool to pick, extract, and filter specific fields from JSON objects using path selectors.', | |
| url: 'https://gist.githack.com/affanpg/84003ccf7d595f251e24d9ccfa0bad93/raw/0c17e3f8a6142c6c39540b79367468535a907727/json-field-picker.html', | |
| gistUrl: 'https://gist.github.com/affanpg/84003ccf7d595f251e24d9ccfa0bad93' | |
| }, | |
| { | |
| id: '6', | |
| title: 'navigator.modelContext Demo', | |
| description: 'A demonstration of the navigator.modelContext API for providing context to the browser\'s built-in AI assistant.', | |
| url: 'https://gist.githack.com/affanpg/2d50d683258e9ef656973133159bead8/raw/26e492215f72671520146c641d8e12613241b1d3/navigator-model-context-demo.html', | |
| gistUrl: 'https://gist.github.com/affanpg/2d50d683258e9ef656973133159bead8' | |
| } | |
| ]; | |
| const GistCard = React.forwardRef(({ result }, forwardedRef) => { | |
| const cardRef = useRef(null); | |
| const [isHovered, setIsHovered] = useState(false); | |
| // This callback ref merges the forwardedRef with the local cardRef | |
| const setRefs = React.useCallback( | |
| (node) => { | |
| // Ref's from useRef are mutable objects that we can assign to | |
| cardRef.current = node; | |
| // However, the forwardedRef can be either a ref object or a callback function | |
| if (typeof forwardedRef === 'function') { | |
| forwardedRef(node); | |
| } else if (forwardedRef) { | |
| forwardedRef.current = node; | |
| } | |
| }, | |
| [forwardedRef] | |
| ); | |
| const handleMouseMove = (e) => { | |
| if (!cardRef.current) return; | |
| const rect = cardRef.current.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| cardRef.current.style.setProperty("--mouse-x", `${x}px`); | |
| cardRef.current.style.setProperty("--mouse-y", `${y}px`); | |
| }; | |
| const isUserscript = result.item.url.endsWith('.user.js') || result.item.title.toLowerCase().includes('userscript'); | |
| const typeLabel = isUserscript ? 'Userscript' : 'Web App'; | |
| const TypeIcon = isUserscript ? FileCode2 : AppWindow; | |
| return ( | |
| <motion.div | |
| ref={setRefs} | |
| layout | |
| initial={{ opacity: 0, scale: 0.95, y: 10 }} | |
| animate={{ opacity: 1, scale: 1, y: 0 }} | |
| exit={{ opacity: 0, scale: 0.95, y: -10 }} | |
| transition={{ duration: 0.3, ease: "easeOut" }} | |
| onMouseMove={handleMouseMove} | |
| onMouseEnter={() => setIsHovered(true)} | |
| onMouseLeave={() => setIsHovered(false)} | |
| whileHover={{ y: -4, scale: 1.01 }} | |
| className={cn( | |
| "relative w-full rounded-2xl bg-white/5 border border-white/10 overflow-hidden group cursor-pointer shadow-lg", | |
| isUserscript ? "hover:shadow-userscript/5" : "hover:shadow-brand/5" | |
| )} | |
| onClick={() => window.open(result.item.url, '_blank')} | |
| > | |
| {/* Spotlight background effect */} | |
| <div | |
| className="pointer-events-none absolute -inset-px rounded-2xl opacity-0 transition-opacity duration-300 group-hover:opacity-100" | |
| style={{ | |
| background: `radial-gradient(600px circle at var(--mouse-x) var(--mouse-y), ${isUserscript ? 'rgba(168, 85, 247, 0.08)' : 'rgba(61, 213, 243, 0.08)'}, transparent 40%)`, | |
| }} | |
| /> | |
| {/* Spotlight border effect */} | |
| <div | |
| className={cn( | |
| "pointer-events-none absolute inset-0 rounded-2xl border opacity-0 transition-opacity duration-300 group-hover:opacity-100", | |
| isUserscript ? "border-userscript" : "border-brand" | |
| )} | |
| style={{ | |
| maskImage: `radial-gradient(300px circle at var(--mouse-x) var(--mouse-y), black, transparent)`, | |
| WebkitMaskImage: `radial-gradient(300px circle at var(--mouse-x) var(--mouse-y), black, transparent)`, | |
| }} | |
| /> | |
| <div className="relative p-6 z-10 flex flex-col h-full"> | |
| <div className="flex justify-between items-start mb-4"> | |
| <div className="flex items-center space-x-3"> | |
| <div className={cn( | |
| "p-2 rounded-xl shadow-inner border", | |
| isUserscript ? "bg-userscript/10 border-userscript/20 text-userscript" : "bg-white/5 border-white/10 text-brand" | |
| )}> | |
| <TypeIcon className="w-4 h-4" /> | |
| </div> | |
| <span className="text-xs font-semibold tracking-widest uppercase text-white/50"> | |
| {typeLabel} | |
| </span> | |
| </div> | |
| <div className="relative w-8 h-8 flex items-center justify-center"> | |
| <motion.div | |
| initial={false} | |
| animate={{ | |
| opacity: isHovered ? 1 : 0, | |
| x: isHovered ? 0 : -10, | |
| scale: isHovered ? 1 : 0.8 | |
| }} | |
| transition={{ duration: 0.2, ease: "easeOut" }} | |
| className={cn( | |
| "absolute inset-0 rounded-full flex items-center justify-center", | |
| isUserscript ? "bg-userscript/20 text-userscript" : "bg-brand/20 text-brand" | |
| )} | |
| > | |
| <ExternalLink className="w-4 h-4" /> | |
| </motion.div> | |
| <motion.div | |
| initial={false} | |
| animate={{ | |
| opacity: isHovered ? 0 : 1, | |
| scale: isHovered ? 0.8 : 1 | |
| }} | |
| transition={{ duration: 0.2 }} | |
| className="absolute inset-0 flex items-center justify-center text-white/20" | |
| > | |
| <ExternalLink className="w-4 h-4" /> | |
| </motion.div> | |
| </div> | |
| </div> | |
| <div className="flex-1 py-2"> | |
| <h2 className={cn( | |
| "text-2xl font-bold text-white mb-3 tracking-tight transition-colors duration-300", | |
| isUserscript ? "group-hover:text-userscript" : "group-hover:text-brand" | |
| )}> | |
| {highlightText( | |
| result.item.title, | |
| result.matches?.filter(m => m.key === 'title').flatMap(m => m.indices), | |
| 'bg-yellow-300 text-ink rounded px-0.5' | |
| )} | |
| </h2> | |
| <p className="text-white/60 text-sm leading-relaxed line-clamp-2"> | |
| {highlightText( | |
| result.item.description, | |
| result.matches?.filter(m => m.key === 'description').flatMap(m => m.indices), | |
| 'bg-yellow-300 text-ink rounded px-0.5' | |
| )} | |
| </p> | |
| </div> | |
| <div className="pt-5 mt-4 border-t border-white/10 flex items-center justify-between"> | |
| <a | |
| href={result.item.gistUrl} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex items-center space-x-2 text-xs font-medium text-white/40 hover:text-white transition-colors z-20 px-3 py-1.5 -ml-3 rounded-md hover:bg-white/5" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| <Github className="w-4 h-4" /> | |
| <span>View Source on GitHub</span> | |
| </a> | |
| </div> | |
| </div> | |
| </motion.div> | |
| ); | |
| }); | |
| const App = () => { | |
| const [query, setQuery] = useState(''); | |
| const searchRef = useRef(null); | |
| const [shortcut, setShortcut] = useState(''); | |
| useEffect(() => { | |
| const isMac = navigator.userAgent.includes('Mac'); | |
| setShortcut(isMac ? '⌘K' : 'Ctrl+K'); | |
| const handler = (event) => { | |
| // Ignore if any modifier other than meta/ctrl with K is used for slash | |
| const key = event.key; | |
| const isSlash = key === '/'; | |
| const isCmdK = (key === 'k' || key === 'K') && (event.metaKey || event.ctrlKey); | |
| if (!isSlash && !isCmdK) return; | |
| // Don't hijack typing in other inputs or editable areas | |
| const active = document.activeElement; | |
| const tag = active && active.tagName ? active.tagName.toLowerCase() : ''; | |
| const isEditable = active && (active.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select'); | |
| if (isEditable) return; | |
| event.preventDefault(); | |
| if (searchRef.current && typeof searchRef.current.focus === 'function') { | |
| searchRef.current.focus(); | |
| } | |
| }; | |
| window.addEventListener('keydown', handler); | |
| return () => window.removeEventListener('keydown', handler); | |
| }, []); | |
| const fuse = useMemo(() => new Fuse(gistData, { | |
| keys: ['title', 'description'], | |
| threshold: 0.4, | |
| ignoreLocation: true, | |
| includeMatches: true // Added for highlighting | |
| }), []); | |
| const filteredGists = useMemo(() => { | |
| if (!query) return gistData.map(item => ({ item })); // Ensure consistent structure for non-search results | |
| return fuse.search(query); // Return full results with matches | |
| }, [query, fuse]); | |
| return ( | |
| <main | |
| className={cn( | |
| 'flex min-h-screen flex-col items-center py-16 px-6', | |
| 'bg-gradient-to-br from-ink via-slate-900 to-cyan-950', | |
| )} | |
| > | |
| <motion.div | |
| initial={{ opacity: 0, y: -20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.5, ease: 'easeOut' }} | |
| className="w-full max-w-2xl" | |
| > | |
| <h1 className="text-4xl font-bold tracking-tight text-center mb-10 text-white"> | |
| Gist App Portal | |
| </h1> | |
| <div className="relative mb-10 group"> | |
| <input | |
| type="text" | |
| className="w-full bg-white/5 border border-white/10 rounded-2xl py-4 pl-14 pr-20 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-brand/50 focus:border-brand/50 focus:bg-white/10 backdrop-blur-md transition-all duration-300 shadow-lg hover:bg-white/10" | |
| placeholder="Search gists by title or description..." | |
| value={query} | |
| onChange={(e) => setQuery(e.target.value)} | |
| ref={searchRef} | |
| /> | |
| <div className="absolute inset-y-0 left-0 pl-5 flex items-center pointer-events-none z-10"> | |
| <Search className="h-5 w-5 text-white/40 group-focus-within:text-brand transition-colors duration-300" /> | |
| </div> | |
| {shortcut && ( | |
| <div className="absolute inset-y-0 right-0 pr-4 flex items-center pointer-events-none z-10"> | |
| <span className="bg-white/10 rounded-lg px-3 py-1.5 text-sm font-medium text-white/70 border border-white/20 shadow-sm backdrop-blur-md flex items-center justify-center gap-0.5 transition-colors duration-300 group-focus-within:border-brand/30 group-focus-within:text-brand/90"> | |
| {shortcut === '⌘K' ? ( | |
| <> | |
| <span className="text-base leading-none">⌘</span> | |
| <span className="leading-none">K</span> | |
| </> | |
| ) : ( | |
| <span>{shortcut}</span> | |
| )} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| <div className="space-y-6"> | |
| <AnimatePresence mode="popLayout"> | |
| {filteredGists.length > 0 ? ( | |
| filteredGists.map((result) => ( | |
| <GistCard key={result.item.id} result={result} /> | |
| )) | |
| ) : ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| className="text-center py-16 text-white/50 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm" | |
| > | |
| <Search className="h-8 w-8 mx-auto mb-4 opacity-50" /> | |
| <p>No gists found matching "{query}"</p> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| </motion.div> | |
| </main> | |
| ); | |
| }; | |
| const rootElement = document.getElementById('app'); | |
| const root = createRoot(rootElement); | |
| root.render(<App />); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment