├── .gitignore ├── README.md ├── index.html ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── App.tsx ├── components │ ├── AccountSelect.tsx │ ├── AccountSelectOption.tsx │ ├── Body.tsx │ ├── Header.tsx │ └── SearchWordsInput.tsx ├── icon │ ├── icon128.png │ ├── icon16.png │ └── icon48.png ├── main.tsx └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | extension 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick Tweet Search Extension 2 | 3 | Chrome Extension to quickly search Tweet by Twitter user 4 | 5 | https://chrome.google.com/webstore/detail/quick-tweet-search/hdkkckfalkjiojdncobdbapmfecmlnbi 6 | 7 | ## Usage 8 | 9 | **Select Twitter user and Input search words** 10 | 11 | スクリーンショット 2022-07-20 1 31 19 12 | 13 | **Search Tweet from that user** 14 | 15 | スクリーンショット 2022-07-20 1 38 24 16 | 17 | ## Feature 18 | - Start by Shortcut 19 | - Lightweight user search 20 | - History of searched users 21 | 22 | ## Setup 23 | 24 | ```shell 25 | npm install 26 | ``` 27 | 28 | ## Build 29 | 30 | ```shell 31 | npm run build 32 | ``` 33 | 34 | ## Run on Dev Server 35 | 36 | ```shell 37 | npm run dev 38 | ``` 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quick Tweet Search 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Quick Tweet Search", 3 | "manifest_version": 3, 4 | "version": "1.0.0", 5 | "description": "Extension to quickly search tweets by Twitter user.", 6 | "permissions": ["storage"], 7 | "action": { 8 | "default_icon": "icon/icon16.png", 9 | "default_popup": "index.html" 10 | }, 11 | "icons": { 12 | "16": "icon/icon16.png", 13 | "48": "icon/icon48.png", 14 | "128": "icon/icon128.png" 15 | }, 16 | "commands": { 17 | "_execute_action": { 18 | "suggested_key": { 19 | "default": "Ctrl+S", 20 | "mac": "Ctrl+S" 21 | }, 22 | "description": "Run extension." 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quick-tweet-search", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@chakra-ui/icons": "^2.0.2", 12 | "@chakra-ui/react": "^2.2.1", 13 | "@emotion/react": "^11.9.3", 14 | "@emotion/styled": "^11.9.3", 15 | "axios": "^0.27.2", 16 | "framer-motion": "^6.4.3", 17 | "lodash.debounce": "^4.0.8", 18 | "react": "^18.0.0", 19 | "react-dom": "^18.0.0", 20 | "react-hook-form": "^7.33.1", 21 | "react-icons": "^4.4.0", 22 | "react-select": "^5.4.0" 23 | }, 24 | "devDependencies": { 25 | "@types/lodash.debounce": "^4.0.7", 26 | "@types/react": "^18.0.0", 27 | "@types/react-dom": "^18.0.0", 28 | "@vitejs/plugin-react": "^2.0.0", 29 | "typescript": "^4.6.3", 30 | "vite": "^3.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from "@chakra-ui/react"; 2 | import { Body } from "./components/Body"; 3 | import { Header } from "./components/Header"; 4 | 5 | const App = () => { 6 | return ( 7 |
8 | 9 |
10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/components/AccountSelect.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import debounce from "lodash.debounce"; 3 | import { FC } from "react"; 4 | import { Control, Controller } from "react-hook-form"; 5 | import Async from "react-select/async"; 6 | import { AccountSelectOption } from "./AccountSelectOption"; 7 | import { Inputs } from "./Body"; 8 | 9 | type Props = { 10 | control: Control; 11 | defaultOptions: Inputs["user"][]; 12 | }; 13 | 14 | type User = { 15 | name: string; 16 | screen_name: string; 17 | profile_image: string; 18 | verified: boolean; 19 | }; 20 | 21 | const _getUsers = ( 22 | value: string | null, 23 | callback: (data: Inputs["user"][]) => void 24 | ) => { 25 | if (!value) return; 26 | 27 | axios 28 | .get(`https://api.taro28.com/twitterUserSearch?keyword=${value}`) 29 | .then((response: { data: User[] }) => 30 | callback( 31 | response.data.map((user) => ({ 32 | value: user.screen_name, 33 | name: user.name, 34 | image: user.profile_image, 35 | verified: user.verified, 36 | })) 37 | ) 38 | ); 39 | }; 40 | 41 | const getUsers = debounce(_getUsers, 200); 42 | 43 | export const AccountSelect: FC = ({ control, defaultOptions }) => { 44 | return ( 45 | ( 49 | ( 59 | 65 | )} 66 | components={{ 67 | IndicatorSeparator: () => null, 68 | DropdownIndicator: () => null, 69 | }} 70 | /> 71 | )} 72 | /> 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/AccountSelectOption.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Box, Flex, Text } from "@chakra-ui/react"; 2 | import { FC } from "react"; 3 | import { MdVerified } from "react-icons/md"; 4 | 5 | type Props = { 6 | name: string; 7 | screenName: string; 8 | image: string; 9 | verified: boolean; 10 | }; 11 | 12 | export const AccountSelectOption: FC = ({ 13 | name, 14 | screenName, 15 | image, 16 | verified, 17 | }) => { 18 | return ( 19 | 20 | 21 | 22 | 23 | 29 | {name} 30 | 31 | {verified && ( 32 | 33 | 34 | 35 | )} 36 | 37 | 38 | @{screenName} 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/Body.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/react"; 2 | import { FC, memo } from "react"; 3 | import { SubmitHandler, useForm } from "react-hook-form"; 4 | import { AccountSelect } from "./AccountSelect"; 5 | import { SearchWordsInput } from "./SearchWordsInput"; 6 | 7 | export type Inputs = { 8 | user: { 9 | value: string; 10 | name: string; 11 | image: string; 12 | verified: boolean; 13 | }; 14 | searchWords: string; 15 | }; 16 | 17 | const LOCAL_STORAGE = { 18 | LAST_SEARCHED_USERS: "lastSearchedUsers", 19 | } as const; 20 | 21 | export const Body: FC = memo(() => { 22 | const rawLastSearchedUsers = 23 | localStorage.getItem(LOCAL_STORAGE.LAST_SEARCHED_USERS) || ""; 24 | const lastSearchedUsers: Inputs["user"][] = rawLastSearchedUsers 25 | ? JSON.parse(rawLastSearchedUsers) 26 | : []; 27 | 28 | const { register, handleSubmit, control } = useForm({ 29 | defaultValues: { 30 | user: lastSearchedUsers[0], 31 | }, 32 | }); 33 | 34 | const handleSearch: SubmitHandler = (data) => { 35 | const query = `${data.user.value ? `(from%3A${data.user.value})` : ""}%20${ 36 | data.searchWords 37 | }`; 38 | window.open( 39 | `https://twitter.com/search?q=${query}&src=typed_query`, 40 | "_blank" 41 | ); 42 | 43 | const nextLastSearchedUsers = Array.from( 44 | // 重複を削除 45 | new Map( 46 | [data.user, ...lastSearchedUsers].map((user) => [user.value, user]) 47 | ).values() 48 | // 検索履歴は5件まで 49 | ).slice(0, 5); 50 | 51 | localStorage.setItem( 52 | LOCAL_STORAGE.LAST_SEARCHED_USERS, 53 | JSON.stringify(nextLastSearchedUsers) 54 | ); 55 | }; 56 | 57 | return ( 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | }); 66 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Center, Heading } from "@chakra-ui/react"; 2 | import { FC, memo } from "react"; 3 | import { FaSearch } from "react-icons/fa"; 4 | 5 | export const Header: FC = memo(() => { 6 | return ( 7 |
8 | 16 | 17 | 18 | 19 | uick Tweet Search 20 | 21 |
22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/SearchWordsInput.tsx: -------------------------------------------------------------------------------- 1 | import { SearchIcon } from "@chakra-ui/icons"; 2 | import { Button, Input, InputGroup, InputRightAddon } from "@chakra-ui/react"; 3 | import { FC } from "react"; 4 | import { UseFormRegisterReturn } from "react-hook-form"; 5 | 6 | type Props = { 7 | registerReturn: UseFormRegisterReturn; 8 | }; 9 | 10 | export const SearchWordsInput: FC = ({ registerReturn }) => { 11 | return ( 12 | 13 | 14 | 15 | 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/icon/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taro-28/quick-tweet-search/2ca8655bdd4f280245c696377940defed048f35f/src/icon/icon128.png -------------------------------------------------------------------------------- /src/icon/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taro-28/quick-tweet-search/2ca8655bdd4f280245c696377940defed048f35f/src/icon/icon16.png -------------------------------------------------------------------------------- /src/icon/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taro-28/quick-tweet-search/2ca8655bdd4f280245c696377940defed048f35f/src/icon/icon48.png -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from "@chakra-ui/react"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | import App from "./App"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | build: { 8 | outDir: "extension", 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------