Letter to my friend
X Growth Strategy
npm install --save-dev @types/chrome
<App/>
- app.tsx
import './App.css' import { useEffect, useState } from 'react'; function App() { const [currentUrl, setCurrentUrl] = useState(''); async function getCurrentTabUrl() { let queryOptions = { active: true, lastFocusedWindow: true }; // `tab` will either be a `tabs.Tab` instance or `undefined`. let tabs = await chrome.tabs.query(queryOptions); return tabs[0]?.url ?? ""; } useEffect(() => { const fetchUrl = async () => { const url = await getCurrentTabUrl(); // Fetch the current tab URL setCurrentUrl(url); }; fetchUrl(); }, []); return ( <div className='text-blue-500'> <h1>Current Tab URL</h1> <p>{currentUrl}</p> </div> ); } export default App;
{ "manifest_version": 3, "name": "React Chrome Extension", "version": "1.0.0", "description": "A simple React app as a Chrome extension", "action": { "default_popup": "index.html" }, "permissions": ["tabs"], // Add Your Bitly Access Token Here "bitly_access_token": "<your-token>" }
function getToken() { const manifestData = chrome.runtime.getManifest(); const bitlyAccessToken = manifestData.bitly_access_token; console.log("Bitly Access Token:", bitlyAccessToken); }
App
Component.inspect
.function
to shorten the URL via Bitly's API:shortenUrl()
- app.tsx
.const shortenUrl = async (longUrl: string) => { const manifestData = chrome.runtime.getManifest(); const token = manifestData.bitly_access_token; const response = await fetch('https://api-ssl.bitly.com/v4/shorten', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ long_url: longUrl }) }); const data = await response.json(); console.log('Shortened URL:', data.link); return data.link; };
App
Component to manage the short URL state :<App/>
- app.tsx
.function App() { const [currentUrl, setCurrentUrl] = useState(''); const [shortUrl, setShortUrl] = useState(''); // Button Handler const handleUrl = async (longUrl: string) => { const shortUrl = await shortenUrl(longUrl) setShortUrl(shortUrl) } ... return ( <div className='text-blue-500'> <h1>Current Tab URL</h1> <p>{currentUrl}</p> // Button to shorten the URL via Bitly's API <button onClick={() => handleUrl(currentUrl)}>Shorten Url</button> <p><span>Bitly Link: </span>{shortUrl}</p> </div> ); }
Card
componentimport { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" export default function Popup() { return ( <Card className="w-[350px]"> <CardHeader> <CardTitle className="font-bold text-2xl">Create Short Link</CardTitle> <CardDescription>Generate a <a href="https://bitly.com" target="_blank">Bitly</a> short link in one-click.</CardDescription> </CardHeader> <CardContent> <div className="space-y-4"> <div className="flex flex-col space-y-1.5"> <Label htmlFor="name">Long URL</Label> <Input defaultValue="https://ui.shadcn.com/docs/components/carousel/"/> <Button className="w-full font-bold" variant="custom" >Generate Short URL</Button> </div> </div> </CardContent> </Card> ) }
Input
, Label
, Card
, and Button
from Shadcn:npx shadcn@latest add input label card button
<Popup/>
:export default function App() { return <Popup/> }
CopyButton
component that will contain the resulting short URL<CopyButton/>
- app.tsx
export function CopyButton({ text = "Hello, world!" }: { text?: string }) { return ( <div className="max-w-sm mx-auto space-y-2"> <div className="flex items-center space-x-2 bg-secondary p-2 rounded-md border-2"> <span className="text-secondary-foreground flex-grow " aria-label="Text to copy"> {text} </span> </div> </div> ) }
Popup
component to include the new addition.<Popup/>
- app.tsx
export default function Popup() { // Dummy Text for Testing const text = "bit.ly/h3ksol" //... <Button className="w-full font-bold" variant="custom" >Generate Short URL</Button> </div> <div className="flex flex-col space-y-1.5"> <CopyButton text={text}/> </div> </div> </CardContent> </Card> ) }
ClearableInput
component that includes a button that enables one-click clearing of the input field.<ClearableInput/>
- app.tsx
export function ClearableInput({ defaultValue }: {defaultValue: string}) { const [inputValue, setInputValue] = useState(defaultValue); // Function to handle input changes const handleInputChange = (e: any) => { setInputValue(e.target.value); }; // Function to clear the input field const handleClear = () => { setInputValue(""); }; return ( <div className="space-y-2"> <div className="flex items-center space-x-2"> <Input type="text" value={inputValue} onChange={handleInputChange} className="p-2 border border-gray-300 rounded-md w-full" /> <Button onClick={handleClear} variant="custom" size="icon" aria-label="Clear input" className="relative bg-transparent border-0 text-black hover:text-white" > <X className=" size-6 fixed z-20 "/> </Button> </div> </div> ); }
CopyButton
to include a one-click copy button and improve user experience.<CopyButton/>
- app.tsx
export function CopyButton({ text = "Hello, world!" }: { text?: string }) { const [isCopied, setIsCopied] = useState(false) const handleCopy = async () => { if (!navigator.clipboard) { console.error('Clipboard API not supported by this browser'); return; } try { await navigator.clipboard.writeText(text); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); } catch (err) { console.error('Failed to copy text: ', err); } } return ( <div className="max-w-sm mx-auto space-y-2"> <div className="flex items-center space-x-2 bg-secondary p-2 rounded-md border-2"> <span className="text-secondary-foreground flex-grow " aria-label="Text to copy"> {text} </span> <Button onClick={handleCopy} aria-label={isCopied ? "Text copied" : "Copy text"} variant="custom" size="icon" className="relative bg-blue-600" > {isCopied ? <Check className="h-4 w-4 fixed z-20 text-green-300" /> : <Copy className="h-4 w-4 fixed z-20 text-white" />} </Button> </div> </div> ) }
prettier-eslint
extension hadn’t been functioning. So I went to their docs and fixed it using the solution below(after troubleshooting):npm i -D prettier@^3.1.0 eslint@^8.52.0 prettier-eslint@^16.1.2 @typescript-eslint/parser@^5.0.1 typescript@^4.4.4
.env.local
file in the root directory and add your bitly access token prefixed with VITE:shortenUrl
function to interact with Bitly’s API.shortenUrl()
- app.tsx
const shortenUrl = async (longUrl: string) => { const token = import.meta.env.VITE_BITLY_TOKEN; const response = await fetch("https://api-ssl.bitly.com/v4/shorten", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ long_url: longUrl, }), }); const data = await response.json(); console.log(data); console.log("Shortened URL:", data.link); return data.link.replace("https://", ""); };
ClearableInput
and Button
in the Popup
component both need to access the state of the long URL.ClearableInput
: It displays the long URL to the user and allows them to update it either by typing in or simply deleting the contents via the clear
button. To accomplish this, it needs to access the long URL’s state
as well as its setState
function.Button
: This button generates the short URL based on the long URL's state
.UrlHandler
. Its purpose would be to create, manage and share the state management among the children components.<URLHandler/>
- app.tsx
state
for the long URL is set up here and passed down to child components.function UrlHandler({ setShortUrl, }: { setShortUrl: Dispatch<React.SetStateAction<string>>; }) { const defaultVal = "https://ui.shadcn.com/docs/components/carousel"; const [longUrl, setLongUrl] = useState(defaultVal); const handleShortUrl = async (longUrl: string) => { const shortUrl = await shortenUrl(longUrl); setShortUrl(shortUrl); }; return ( <div className="flex flex-col space-y-1.5"> <Label htmlFor="name">Long URL</Label> <ClearableInput inputVal={longUrl} setInputVal={setLongUrl} /> <Button className="w-full font-bold" variant="custom" onClick={() => handleShortUrl(longUrl)} > Generate Short URL </Button> </div> ); }
<Popup/>
- app.tsx
ClearableInput
and Button
(that generates the short URL) have now been removed.export default function Popup() { const text = "bit.ly/4ee2kgf"; const [shortUrl, setShortUrl] = useState(text); return ( <Card className="rounded-none"> <CardHeader> <CardTitle className="font-bold text-2xl">Create Short Link</CardTitle> <CardDescription> Generate a{" "} <a href="https://bitly.com" target="_blank"> Bitly </a>{" "} short link in one-click. </CardDescription> </CardHeader> <CardContent> <div className="space-y-4"> <UrlHandler setShortUrl={setShortUrl} /> <div className="flex flex-col space-y-1.5"> <CopyButton text={shortUrl} /> </div> </div> </CardContent> </Card> ); }
<ClearableInput/>
- app.tsx
inputVal
and setInputVal
are passed to it to manage the state of the long URL.export function ClearableInput({ inputVal, setInputVal, }: { inputVal: string; setInputVal: Dispatch<React.SetStateAction<string>>; }) { // Function to handle input changes const handleInputChange = (e: any) => { setInputVal(e.target.value); }; // Function to clear the input field const handleClear = () => { setInputVal(""); }; return ( <div className="space-y-2"> <div className="flex items-center space-x-2"> <Input type="text" value={inputVal} onChange={handleInputChange} className="p-2 border border-gray-300 rounded-md w-full" /> <Button onClick={handleClear} variant="custom" size="icon" aria-label="Clear input" className="relative bg-transparent border-0 text-black hover:text-white" > <X className=" size-6 fixed z-20 " /> </Button> </div> </div> ); }
import { z } from "zod"; // creating a schema for strings const mySchema = z.string(); // parsing mySchema.parse("tuna"); // => "tuna" mySchema.parse(12); // => throws ZodError // "safe" parsing (doesn't throw error if validation fails) mySchema.safeParse("tuna"); // => { success: true; data: "tuna" } mySchema.safeParse(12); // => { success: false; error: ZodError }
src/components
folder and moving all functions to src/lib/utils.ts
.validateUrl()
and <Error/>
.export function validateUrl(url: string) { const urlSchema = z .string() .url() .startsWith("https://", { message: "Invalid/Insecure URL" }); const res = urlSchema.safeParse(url); return res.success ? res.data : res.error.format()._errors[0]; } export function Error({ text }: { text: string }) { return <p className="text-red-500 font-bold">{text + "!"}</p>; }
shortenUrl
function.shortenUrl()
const shortenUrl = async (longUrl: string) => { const token = import.meta.env.VITE_BITLY_TOKEN; const res = validateUrl(longUrl); console.log(res); return longUrl === res ? "Success!" : <Error text={res} />; //.. }
popup.tsx
.validateUrl()
function, now in the utils.ts
file.export function validateUrl(url: string) { const urlSchema = z .string() .url() .startsWith("https://", { message: "Invalid/Insecure URL" }); try { const res = urlSchema.parse(url); return res; } catch (err: any) { if (err instanceof z.ZodError) { throw new Error(err.format()._errors[0]); } throw new Error("Something went wrong"); } }
validateUrl()
is called by the updated <URLHandler/>
- handleShortUrl()
.validateUrl
is caught and set as the shortUrl
since we display errors in the same container.export default function UrlHandler({ setShortUrl, }: { setShortUrl: Dispatch<React.SetStateAction<TShortUrl>>; }) { const defaultVal = "https://ui.shadcn.com/docs/components/carousel"; const [longUrl, setLongUrl] = useState(defaultVal); const handleShortUrl = async (longUrl: string) => { try { const res = validateUrl(longUrl); const shortUrlString = await shortenUrl(res); setShortUrl({ text: shortUrlString, success: true }); } catch (err: any) { setShortUrl({ text: err.message as string, success: false }); } }; return ( <div className="flex flex-col space-y-1.5"> <Label htmlFor="name">Long URL</Label> <ClearableInput inputVal={longUrl} setInputVal={setLongUrl} /> <Button className="w-full font-bold" variant="custom" onClick={() => handleShortUrl(longUrl)} > Generate Short URL </Button> </div> ); }
<CopyButton/>
is equally updated to address the new changes.Success
from Error
.import { Check, Copy } from "lucide-react"; import { useState } from "react"; import { Button } from "./ui/button"; type TShortUrl = { text: string; success: boolean }; export default function CopyButton({ text, success }: TShortUrl) { const [isCopied, setIsCopied] = useState(false); const handleCopy = async () => { if (!navigator.clipboard) { console.error("Clipboard API not supported by this browser"); return; } try { await navigator.clipboard.writeText(text); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); } catch (err) { console.error("Failed to copy text: ", err); } }; return ( <div className="mx-auto space-y-2"> <div className="flex items-center space-x-2 bg-secondary p-2 rounded-md border-2"> <span className="text-secondary-foreground flex-grow " aria-label="Text to copy" > {success ? <Success text={text} /> : <Error text={text} />} </span> <Button onClick={handleCopy} aria-label={isCopied ? "Text copied" : "Copy text"} variant="custom" size="icon" className="relative bg-blue-600" > {isCopied ? ( <Check className="h-4 w-4 fixed z-20 text-green-300" /> ) : ( <Copy className="h-4 w-4 fixed z-20 text-white" /> )} </Button> </div> </div> ); } function Success({ text }: { text: string }) { return <p className="text-blue-400 font-light text-sm">{text}</p>; } export function Error({ text }: { text: string }) { return <p className="text-red-500 font-bold">{text + "!"}</p>; }
shortenUrl()
is simplified temporarily(Disconnect the Bitly API).export const shortenUrl = async (longUrl: string) => { return "bit.ly/jlo22"; // ... }
shortenUrl
function.function shortenURLBrowser(longURL: string) { const shortenedURLs = JSON.parse( localStorage.getItem("shortenedURLs") || "{}" ); if (shortenedURLs[longURL]) { console.log("This URL has already been shortened:", shortenedURLs[longURL]); return shortenedURLs[longURL]; } else { const randomString = genRandString(); const shortURL = `bit.ly/${randomString}`; // Save the new shortened URL shortenedURLs[longURL] = shortURL; localStorage.setItem("shortenedURLs", JSON.stringify(shortenedURLs)); console.log("URL shortened successfully:", shortURL); return shortURL; } } export const shortenUrl = async (longUrl: string) => { return shortenURLBrowser(longUrl); //... }
<URLHandler/>
useEffect
Hook now updates the state of the input field based on the current tab.import { Dispatch, useState, useEffect } from "react"; import { Label } from "./ui/label"; import ClearableInput from "./clearable-input"; import { Button } from "./ui/button"; import { TShortUrl } from "@/lib/types"; import { shortenUrl, validateUrl } from "@/lib/utils"; const defaultVal = "https://ui.shadcn.com/docs/components/carousel"; async function getCurrentTabUrl() { const queryOptions = { active: true, lastFocusedWindow: true }; const tabs = await chrome.tabs.query(queryOptions); return tabs[0]?.url ?? defaultVal; } export default function UrlHandler({ setShortUrl, }: { setShortUrl: Dispatch<React.SetStateAction<TShortUrl>>; }) { const [longUrl, setLongUrl] = useState(defaultVal); useEffect(() => { const fetchUrl = async () => { const url = await getCurrentTabUrl(); // Fetch the current tab URL setLongUrl(url); }; fetchUrl(); }, []); const handleShortUrl = async (longUrl: string) => { try { const res = validateUrl(longUrl); const shortUrlString = await shortenUrl(res); setShortUrl({ text: shortUrlString, success: true }); } catch (err: any) { setShortUrl({ text: err.message as string, success: false }); } }; return ( <div className="flex flex-col space-y-1.5"> <Label htmlFor="name">Long URL</Label> <ClearableInput inputVal={longUrl} setInputVal={setLongUrl} /> <Button className="w-full font-bold" variant="custom" onClick={() => handleShortUrl(longUrl)} > Generate Short URL </Button> </div> ); }