🌱 create projects

This commit is contained in:
kawamataryo 2023-05-15 16:20:51 +09:00
commit 4d46a620e4
22 changed files with 15524 additions and 0 deletions

34
.github/workflows/submit.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: "Submit to Web Store"
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache pnpm modules
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: pnpm/action-setup@v2.2.4
with:
version: latest
run_install: true
- name: Use Node.js 16.x
uses: actions/setup-node@v3.4.1
with:
node-version: 16.x
cache: "pnpm"
- name: Build the extension
run: pnpm build
- name: Package the extension into a zip artifact
run: pnpm package
- name: Browser Platform Publish
uses: PlasmoHQ/bpp@v3
with:
keys: ${{ secrets.SUBMIT_KEYS }}
artifact: build/chrome-mv3-prod.zip

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
#cache
.turbo
.next
.vercel
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*
out/
build/
dist/
# plasmo - https://www.plasmo.com
.plasmo
# bpp - http://bpp.browser.market/
keys.json
# typescript
.tsbuildinfo

17
.prettierrc.cjs Normal file
View File

@ -0,0 +1,17 @@
/**
* @type {import('prettier').Options}
*/
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: false,
trailingComma: "none",
bracketSpacing: true,
bracketSameLine: true,
plugins: [require.resolve("@plasmohq/prettier-plugin-sort-imports")],
importOrder: ["^@plasmohq/(.*)$", "^~(.*)$", "^[./]"],
importOrderSeparation: true,
importOrderSortSpecifiers: true
}

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"cSpell.words": [
"Bluesky",
"BSKY"
]
}

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# Sky Follower BridgeWip
Instantly find and follow the same users from your Twitter follower on Bluesky.
https://github.com/kawamataryo/sky-follower-bridge/assets/11070996/0c87f9b9-573f-48c3-b7ba-f54e0e30a7e7
## 📦 Installation
- [Chrome Web Store]()
- [Edge Add Ons]()
## 🚀 How to use
1. Open your Twitter [Follower](https://twitter.com/following) or [Follow](https://twitter.com/followers) page.
2. Launch the Sky Follower Bridge Chrome extension.
3. Input your Bluesky ID and password.
4. Execute the user search.
5. Bluesky users will appear in the Follower list.
6. Click the Follow button to follow them on Bluesky.
## 🚨 Limitations
Note that due to Twitters limitations, its not possible to search all Followers at once. If you want to search for more, click the Find More button that appears on the screen.
<img width="665" alt="CleanShot 2023-05-20 at 17 40 27@2x" src="https://github.com/kawamataryo/sky-follower-bridge/assets/11070996/f9780284-a60c-401b-8ee8-fca25750da06">

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

1
assets/icon.popup.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="4"><path stroke-linecap="round" d="M36 8H13c-3 0-9 2-9 8s6 8 9 8h22c3 0 9 2 9 8s-6 8-9 8H12"/><path d="M40 12a4 4 0 1 0 0-8a4 4 0 0 0 0 8ZM8 44a4 4 0 1 0 0-8a4 4 0 0 0 0 8Z"/></g></svg>

After

Width:  |  Height:  |  Size: 344 B

14719
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "sky-follower-bridge",
"displayName": "Sky Follower Bridge",
"version": "0.1.0",
"description": "Instantly find and follow the same users from your Twitter follower on Bluesky.",
"author": "kawamataryou",
"scripts": {
"dev": "plasmo dev",
"build": "plasmo build",
"package": "plasmo package"
},
"dependencies": {
"@atproto/api": "^0.3.3",
"@plasmohq/messaging": "^0.3.0",
"@plasmohq/storage": "^1.6.0",
"plasmo": "0.67.4",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@plasmohq/prettier-plugin-sort-imports": "3.6.4",
"@types/chrome": "0.0.210",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"prettier": "2.8.3",
"tailwindcss": "^3.3.2",
"daisyui": "^2.51.6",
"typescript": "4.9.4"
},
"manifest": {
"host_permissions": [
"https://bsky.social/*",
"https://twitter.com/*"
]
},
"volta": {
"node": "16.20.0"
}
}

9
postcss.config.js Normal file
View File

@ -0,0 +1,9 @@
/**
* @type {import('postcss').ProcessOptions}
*/
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

111
src/content.ts Normal file
View File

@ -0,0 +1,111 @@
import { BskyClient } from "./lib/bskyClient";
import type { PlasmoCSConfig } from "plasmo"
import { MESSAGE_NAMES } from "~lib/constants";
import { getAccountNameAndDisplayName, getUserCells, insertBskyProfileEl, insertNotFoundEl, insertReloadEl, removeReloadElIfExists } from "~lib/domHelpers";
import { isSimilarUser } from "~lib/bskyHelpers";
import "./style.content.css"
import { debugLog } from "~lib/utils";
export const config: PlasmoCSConfig = {
matches: ["https://twitter.com/*"],
all_frames: true
}
let abortController = new AbortController();
const notFoundUserCache = new Set<string>()
const initialize = async () => {
abortController.abort()
abortController = new AbortController()
}
const searchBskyUsers = async ({
userId,
password
}) => {
removeReloadElIfExists()
const agent = await BskyClient.createAgent({
identifier: userId,
password: password,
});
const userCells = getUserCells()
debugLog(`userCells length: ${userCells.length}`)
for (const [index, userCell] of userCells.entries()) {
const { twAccountName, twDisplayName } = getAccountNameAndDisplayName(userCell)
if (notFoundUserCache.has(twAccountName)) {
insertNotFoundEl(userCell)
continue
}
const [searchResultByAccountName] = await agent.searchUser({
term: twAccountName,
limit: 1,
})
// first, search by account name
if (isSimilarUser(twDisplayName, searchResultByAccountName) || isSimilarUser(twAccountName, searchResultByAccountName)) {
insertBskyProfileEl({
dom: userCell,
profile: searchResultByAccountName,
abortController,
clickAction: async () => { await agent.follow(searchResultByAccountName.did) }
})
} else {
// if not found, search by display name
const [searchResultByDisplayName] = await agent.searchUser({
term: twDisplayName,
limit: 1,
})
if (isSimilarUser(twDisplayName, searchResultByDisplayName) || isSimilarUser(twAccountName, searchResultByDisplayName)) {
insertBskyProfileEl({
dom: userCell,
profile: searchResultByDisplayName,
abortController,
clickAction: async () => { await agent.follow(searchResultByAccountName.did) }
})
} else {
insertNotFoundEl(userCell)
notFoundUserCache.add(twAccountName)
}
}
if (process.env.NODE_ENV === "development" && index > 100) {
break
}
}
// if there are more users, insert reload button
const finishedUserCells = getUserCells({
filterInsertedElement: false
})
if (finishedUserCells.at(-1) !== userCells.at(-1)) {
insertReloadEl(async () => {
await searchBskyUsers({
userId,
password,
})
})
}
}
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
if (message.name === MESSAGE_NAMES.SEARCH_BSKY_USER) {
initialize()
searchBskyUsers({
userId: message.body.userId,
password: message.body.password
}).then(() => {
sendResponse({ hasError: false })
}).catch((e) => {
sendResponse({ hasError: true, message: e.toString() })
})
return true
}
return false
})

9
src/images/logo.svg Normal file
View File

@ -0,0 +1,9 @@
<svg width="280" height="266" viewBox="0 0 280 266" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M111.625 111.5C96.3625 111.5 83.2969 106.066 72.4281 95.1969C61.5594 84.3281 56.125 71.2625 56.125 56C56.125 40.7375 61.5594 27.6719 72.4281 16.8031C83.2969 5.93438 96.3625 0.5 111.625 0.5C126.887 0.5 139.953 5.93438 150.822 16.8031C161.691 27.6719 167.125 40.7375 167.125 56C167.125 71.2625 161.691 84.3281 150.822 95.1969C139.953 106.066 126.887 111.5 111.625 111.5ZM255.925 261.35L221.237 226.663C216.381 229.438 211.178 231.75 205.628 233.6C200.078 235.45 194.181 236.375 187.937 236.375C170.594 236.375 155.854 230.302 143.718 218.157C131.582 206.012 125.509 191.272 125.5 173.938C125.5 156.594 131.573 141.849 143.718 129.704C155.863 117.559 170.603 111.491 187.937 111.5C205.281 111.5 220.026 117.573 232.171 129.718C244.316 141.863 250.384 156.603 250.375 173.938C250.375 180.181 249.45 186.078 247.6 191.628C245.75 197.178 243.437 202.381 240.662 207.238L275.35 241.925C277.894 244.469 279.166 247.706 279.166 251.638C279.166 255.569 277.894 258.806 275.35 261.35C272.806 263.894 269.569 265.166 265.637 265.166C261.706 265.166 258.469 263.894 255.925 261.35ZM187.937 208.625C197.65 208.625 205.859 205.272 212.566 198.566C219.272 191.859 222.625 183.65 222.625 173.938C222.625 164.225 219.272 156.016 212.566 149.309C205.859 142.603 197.65 139.25 187.937 139.25C178.225 139.25 170.016 142.603 163.309 149.309C156.603 156.016 153.25 164.225 153.25 173.938C153.25 183.65 156.603 191.859 163.309 198.566C170.016 205.272 178.225 208.625 187.937 208.625ZM28.375 222.5C20.7437 222.5 14.2086 219.781 8.76962 214.342C3.33062 208.903 0.615743 202.372 0.624993 194.75V183.997C0.624993 176.134 2.59062 168.85 6.52187 162.144C10.4531 155.438 15.8875 150.35 22.825 146.881C34.6187 140.869 47.9156 135.781 62.7156 131.619C77.5156 127.456 93.9344 125.375 111.972 125.375C107.347 132.544 103.818 140.235 101.385 148.449C98.9525 156.663 97.7407 165.159 97.75 173.938C97.75 182.494 98.9664 190.879 101.399 199.093C103.832 207.307 107.351 215.109 111.958 222.5H28.375Z" fill="url(#paint0_linear_617_24)"/>
<defs>
<linearGradient id="paint0_linear_617_24" x1="18.5" y1="139" x2="246" y2="142.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#1D99F0"/>
<stop offset="1" stop-color="#1D52DA"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

39
src/lib/bskyClient.ts Normal file
View File

@ -0,0 +1,39 @@
import { AppBskyFeedPost, AppBskyRichtextFacet, BskyAgent } from "@atproto/api";
export class BskyClient {
private service = "https://bsky.social";
agent: BskyAgent;
private constructor() {
this.agent = new BskyAgent({ service: this.service });
}
public static async createAgent({
identifier,
password,
}: {
identifier: string;
password: string;
}): Promise<BskyClient> {
const client = new BskyClient();
await client.agent.login({ identifier, password });
return client;
}
public searchUser = async ({
term,
limit,
}: {
term: string;
limit: number;
}) => {
const result = await this.agent.searchActors({
term,
limit,
});
return result.data.actors;
};
public follow = async (subjectDid: string) => {
await this.agent.follow(subjectDid);
}
}

17
src/lib/bskyHelpers.ts Normal file
View File

@ -0,0 +1,17 @@
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs"
export const isSimilarUser = (name: string, bskyProfile: ProfileView | undefined) => {
if(!bskyProfile) { return false }
const lowerCaseName = name.toLocaleLowerCase()
if(lowerCaseName === bskyProfile?.handle.toLocaleLowerCase().replace("@", "").split('.')[0]) {
return true
}
if(lowerCaseName === bskyProfile.displayName?.toLocaleLowerCase()) {
return true
}
if(bskyProfile.description?.toLocaleLowerCase().includes(lowerCaseName)) {
return true
}
return false
}

19
src/lib/constants.ts Normal file
View File

@ -0,0 +1,19 @@
export const MESSAGE_NAMES = {
SEARCH_BSKY_USER: "search_bsky_user"
}
const STORAGE_PREFIX = "sky_follower_bridge_storage"
export const STORAGE_KEYS = {
BSKY_USER_ID: `${STORAGE_PREFIX}_bsky_password`,
BSKY_PASSWORD: `${STORAGE_PREFIX}_bsky_user`,
}
export const TARGET_URLS_REGEX = [
/^https:\/\/twitter\.com\/[^/]+\/following$/,
/^https:\/\/twitter\.com\/[^/]+\/followers$/,
]
export const MESSAGE_TYPE = {
ERROR: "error",
SUCCESS: "success",
} as const

107
src/lib/domHelpers.ts Normal file
View File

@ -0,0 +1,107 @@
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs"
export const getUserCells = ({ filterInsertedElement }: { filterInsertedElement: boolean } = { filterInsertedElement: true }) => {
const userCells = document.querySelectorAll('[data-testid="primaryColumn"] [data-testid="UserCell"]');
// filter out already inserted elements
if (filterInsertedElement) {
return Array.from(userCells).filter((userCell) => {
const nextElement = userCell.nextElementSibling
if (!nextElement) { return true }
return nextElement.classList.contains("bsky-user-content") === false
})
} else {
return Array.from(userCells)
}
}
export const insertReloadEl = (clickAction: () => void) => {
const lastInsertedEl = Array.from(document.querySelectorAll('.bsky-user-content')).at(-1)
lastInsertedEl.insertAdjacentHTML('afterend', `
<div class="bsky-reload-btn-wrapper">
<button class="bsky-reload-btn">
Find More
</button>
</div>
`)
const reloadBtn = document.querySelector(".bsky-reload-btn") as HTMLElement
reloadBtn.addEventListener("click", async (e) => {
const target = e.target as HTMLButtonElement
if (target.classList.contains('bsky-reload-btn__processing')) {
return
}
await clickAction()
})
}
export const removeReloadElIfExists = () => {
const reloadBtnWrapper = document.querySelector(".bsky-reload-btn-wrapper") as HTMLElement
reloadBtnWrapper?.remove()
}
export const getAccountNameAndDisplayName = (userCell: Element) => {
const [avatarEl, displayNameEl] = userCell?.querySelectorAll("a")
const twAccountName = avatarEl?.getAttribute("href")?.replace("/", "")
const twDisplayName = displayNameEl?.textContent
return { twAccountName, twDisplayName }
}
export const insertBskyProfileEl = ({ dom, profile, abortController, clickAction }: { dom: Element, profile: ProfileView, abortController: AbortController, clickAction: () => void }) => {
const avatarEl = profile.avatar ? `<img src="${profile.avatar}" width="48" />` : "<div class='no-avatar'></div>"
const followButtonEl = profile.viewer?.following ? "<button class='follow-button follow-button__following'>Following on Bluesky</button>" : "<button class='follow-button'>Follow on Bluesky</button>"
dom.insertAdjacentHTML('afterend', `
<div class="bsky-user-content">
<div class="icon-section">
<a href="https://bsky.app/profile/${profile.handle}" target="_blank" rel="noopener">
${avatarEl}
</a>
</div>
<div class="content">
<div class="name-and-controller">
<div>
<p class="display-name"><a href="https://bsky.app/profile/${profile.handle}" target="_blank" rel="noopener">${profile.displayName ?? profile.handle}</a></p>
<p class="handle">@${profile.handle}</p>
</div>
<div>
${followButtonEl}
</div>
</div>
${profile.description ? `<p class="description">${profile.description}</p>` : ""}
</div>
<simple-greeting></simple-greeting>
</div>
`)
dom.nextElementSibling?.addEventListener('click', async (e) => {
// TODO: Add unfollow action
const target = e.target as Element
const classList = target.classList
if (classList.contains('follow-button') && !classList.contains('follow-button__following')) {
target.textContent = "processing..."
target.classList.add('follow-button__processing')
await clickAction()
target.textContent = "Following on Bluesky"
target.classList.remove('follow-button__processing')
target.classList.add('follow-button__following')
}
}, {
signal: abortController.signal
})
}
export const insertNotFoundEl = (dom: Element) => {
dom.insertAdjacentHTML('afterend', `
<div class="bsky-user-content bsky-user-content__not-found">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /></svg>
<p class="not-found">No similar users found.</p>
</div>
`)
}
export const cleanBskyUserElements = () => {
const bskyUserContent = document.querySelectorAll('.bsky-user-content');
if (bskyUserContent.length > 0) {
bskyUserContent.forEach((el) => {
el.remove()
})
}
}

5
src/lib/utils.ts Normal file
View File

@ -0,0 +1,5 @@
export const debugLog = (message: string) => {
if(process.env.NODE_ENV === "development") {
console.log(`🔷 [Sky Follower Bridge] ${message}`)
}
}

141
src/popup.tsx Normal file
View File

@ -0,0 +1,141 @@
import { FormEvent, useState } from "react"
import iconSrc from "data-base64:~assets/icon.popup.svg"
import "./style.css"
import { sendToContentScript } from "@plasmohq/messaging"
import { MESSAGE_NAMES, MESSAGE_TYPE, STORAGE_KEYS, TARGET_URLS_REGEX } from "~lib/constants"
import { useStorage } from "@plasmohq/storage/hook"
import { debugLog } from "./lib/utils"
function IndexPopup() {
const [isLoading, setIsLoading] = useState(false)
const [password, setPassword] = useStorage(STORAGE_KEYS.BSKY_PASSWORD, "")
const [userId, setUserId] = useStorage(STORAGE_KEYS.BSKY_USER_ID, "")
const [message, setMessage] = useState<null | {
type: typeof MESSAGE_TYPE[keyof typeof MESSAGE_TYPE]
message: string
}>(null)
const isDisabled = !password || !userId || isLoading
const isShowErrorMessage = message?.type === MESSAGE_TYPE.ERROR
const isShowSuccessMessage = message?.type === MESSAGE_TYPE.SUCCESS
const setErrorMessage = (message: string) => { setMessage({ type: MESSAGE_TYPE.ERROR, message }) }
const isExecutablePage = async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
return TARGET_URLS_REGEX.some((r) => r.test(tab.url))
}
const searchBskyUser = async (e: FormEvent) => {
e.preventDefault()
if(!await isExecutablePage()) {
setErrorMessage("Error: Invalid page. please open the Twitter followers or following page.")
return;
}
setMessage(null)
setIsLoading(true)
try {
const res: { hasError: boolean, message: string } = await sendToContentScript({
name: MESSAGE_NAMES.SEARCH_BSKY_USER,
body: {
password,
userId,
}
})
if(res.hasError) {
setErrorMessage(res.message)
} else {
setMessage({ type: MESSAGE_TYPE.SUCCESS, message: "Completed. Try again if no results found.”" })
}
} catch(e) {
setErrorMessage(e.toString())
} finally {
setIsLoading(false)
}
}
return (
<div className="px-5 pt-3 pb-4 w-[330px]">
<h1 className="text-primary text-2xl font-thin flex gap-2 items-center"><svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="4"><path stroke-linecap="round" d="M36 8H13c-3 0-9 2-9 8s6 8 9 8h22c3 0 9 2 9 8s-6 8-9 8H12"/><path d="M40 12a4 4 0 1 0 0-8a4 4 0 0 0 0 8ZM8 44a4 4 0 1 0 0-8a4 4 0 0 0 0 8Z"/></g></svg>Sky Follower Bridge</h1>
<form onSubmit={searchBskyUser} className="mt-2">
<label className="input-group input-group-lg">
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</span>
<input
type="text"
placeholder="bluesky account id"
value={userId}
onChange={(e) => setUserId(e.target.value)}
className="input input-bordered input-sm w-full max-w-xs"
/>
</label>
<label className="input-group input-group-lg mt-2">
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
</span>
<input
type="password"
placeholder="bluesky app password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className=" input input-bordered input-sm w-full max-w-xs"
/>
</label>
<button
type="submit"
className={`disabled:text-gray-600 mt-3 normal-case btn btn-primary btn-sm w-full ${isLoading ? "loading" : ""}`}
disabled={isDisabled}>
Find Bluesky Users
</button>
{isShowErrorMessage && (
<div className="alert text-red-600 border border-red-600 p-2 rounded-md mt-2">
<div>
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>{message.message}</span>
</div>
</div>
)}
{isShowSuccessMessage && (
<div className="alert text-green-600 border border-green-600 p-1 rounded-md mt-2">
<div>
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>Success. Try again if no results found.</span>
</div>
</div>
)}
</form>
</div>
)
}
export default IndexPopup

147
src/style.content.css Normal file
View File

@ -0,0 +1,147 @@
/* ----------------- */
/* base */
/* ----------------- */
:root {
--bsky-primary-color: rgb(29, 78, 216);
--bsky-primary-hover-color: #2563eb;
}
.bsky-user-content {
background: rgb(2,0,36);
background: var(--bsky-primary-color);
color: #fff;
display: flex;
gap: 15px;
padding: 12px 16px;
}
.bsky-user-content p {
margin: 0;
}
/* ----------------- */
/* icon */
/* ----------------- */
.bsky-user-content .icon-section {
display: grid;
place-content: center;
}
.bsky-user-content .icon-section a{
text-decoration: none;
}
.bsky-user-content .icon-section img {
border-radius: 50%;
}
.bsky-user-content .icon-section .no-avatar {
width: 48px;
height: 48px;
background: #ccc;
border-radius: 50%;
}
/* ----------------- */
/* card content */
/* ----------------- */
.bsky-user-content .content {
width: 100%;
}
.bsky-user-content .content .display-name a {
font-size: 15px;
font-weight: bold;
color: #fff;
text-decoration: none;
}
.bsky-user-content .content .handle {
font-size: 14px;
color: #ccc;
}
.bsky-user-content .content .descrption {
font-size: 12px;
}
.name-and-controller {
display: flex;
gap: 10px;
justify-content: space-between;
}
.name-and-controller .follow-button {
border: 1px solid #fff;
padding: 6px 30px;
font-size: 14px;
border: 1px solid #fff;
background: #fff;
color: var(--bsky-primary-color);
border-radius: 27px;
cursor: pointer;
}
.name-and-controller .follow-button__following {
background: transparent;
color: #fff;
cursor: auto;
}
.name-and-controller .follow-button__processing {
background: rgb(255,255,255, 0.3);;
color: #fff;
cursor: auto;
}
/* ----------------- */
/* not found card */
/* ----------------- */
.bsky-user-content__not-found {
padding: 4px 16px;
background: #333;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
}
.bsky-user-content__not-found svg {
width: 16px;
}
/* ----------------- */
/* reload btn */
/* ----------------- */
.bsky-reload-btn-wrapper {
padding: 24px 0;
text-align: center;
}
.bsky-reload-btn {
padding: 8px 16px;
width: 280px;
font-size: 14px;
border: none;
border-radius: 5px;
background-color: var(--bsky-primary-color);
color: #fff;
top: 60px;
cursor: pointer;
animation: btnwrapanime 1.5s infinite;
box-shadow: 0 0 0 0 rgb(0, 53, 188);
}
.bsky-reload-btn:hover {
animation: none;
background-color: var(--bsky-primary-hover-color);
}
.bsky-reload-btn__processing {
background-color: var(--bsky-primary-hover-color);
animation: none;
cursor: auto;
}
@keyframes btnwrapanime {
70% {
box-shadow: 0 0 0 20px rgba(233, 30, 99, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(233, 30, 99, 0);
}
}

3
src/style.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

12
tailwind.config.js Normal file
View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
mode: "jit",
darkMode: "class",
content: ["./src/**/*.tsx"],
plugins: [
require("daisyui")
],
daisyui: {
themes: ["winter"],
},
}

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"extends": "plasmo/templates/tsconfig.base",
"exclude": [
"node_modules"
],
"include": [
".plasmo/index.d.ts",
".plasmo/**/*",
"./**/*.ts",
"./**/*.tsx"
],
"compilerOptions": {
"paths": {
"~*": [
"./src/*"
]
},
"baseUrl": ".",
"experimentalDecorators": true
}
}