├── .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 |
12 |
13 | **Search Tweet from that user**
14 |
15 |
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 |
--------------------------------------------------------------------------------