1/** @jsxImportSource https://esm.sh/react@18.2.0 */
2import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
3import React from "https://esm.sh/react@18.2.0";
4
5/**
8function App() {
9 // --- State declarations ---
10 const [targetAccount, setTargetAccount] = React.useState("");
11 const [query, setQuery] = React.useState("");
12 const [searchType, setSearchType] = React.useState("Latest");
13 const [isDarkMode, setIsDarkMode] = React.useState(false);
14 const [maxTweets, setMaxTweets] = React.useState("");
15
16 // Date filters
17 const [startYear, setStartYear] = React.useState("");
18 const [startMonth, setStartMonth] = React.useState("");
19 const [startDay, setStartDay] = React.useState("");
20 const [endYear, setEndYear] = React.useState("");
21 const [endMonth, setEndMonth] = React.useState("");
22 const [endDay, setEndDay] = React.useState("");
23
24 // Include options
25 const [includeReplies, setIncludeReplies] = React.useState(false);
26 const [includeRetweets, setIncludeRetweets] = React.useState(false);
27
28 // Count filters - removed direction states, as they'll always be "greater than or equal to"
29 const [likesCount, setLikesCount] = React.useState("");
30 const [retweetsCount, setRetweetsCount] = React.useState("");
31 const [repliesCount, setRepliesCount] = React.useState("");
32 const [bookmarksCount, setBookmarksCount] = React.useState("");
33 const [viewsCount, setViewsCount] = React.useState("");
34
35 // UI state
36 const [showAdvanced, setShowAdvanced] = React.useState(false);
37 const [tweets, setTweets] = React.useState([]);
38 const [loading, setLoading] = React.useState(false);
39 const [loadingMessage, setLoadingMessage] = React.useState("");
40 const [error, setError] = React.useState(null);
41 const [currentSearchQuery, setCurrentSearchQuery] = React.useState("");
42 const [parentOrigin, setParentOrigin] = React.useState("");
43
44 // ツイート選択用の状態
45 const [selectedTweets, setSelectedTweets] = React.useState({});
46 const [allSelected, setAllSelected] = React.useState(false);
47
48 const isFetchingRef = React.useRef(false);
49 const sourceUrl = import.meta.url.replace("esm.town", "val.town");
50
58
59 // Initialize dark mode on mount
60 React.useEffect(() => {
61 document.documentElement.classList.toggle("light-mode", !isDarkMode);
62 }, []);
63
64 // Close modal when clicking outside
65 const modalRef = React.useRef(null);
66 React.useEffect(() => {
67 const handleClickOutside = (event) => {
68 if (modalRef.current && !modalRef.current.contains(event.target)) {
80
81 // --- Setup postMessage communication with parent frame ---
82 React.useEffect(() => {
83 // Listen for messages from parent window
84 const handleMessage = (event) => {
189
190 // --- Helper Functions ---
191 const buildQuery = React.useCallback(() => {
192 // Start with the account filter (required)
193 const accountName = targetAccount.trim().replace(/^@/, "");
485 const selectedTweetCount = Object.keys(selectedTweets).length;
486
487 // --- Render Main Application UI with React.createElement ---
488 return React.createElement(
489 "div",
490 { className: "app" },
491 React.createElement(
492 "div",
493 { className: "header-container" },
494 React.createElement("h1", { className: "title" }, "X Account Search"),
495 React.createElement(
496 "button",
497 {
504 // 2. アイコンの表示ロジックを入れ替え
505 isDarkMode
506 ? React.createElement(
507 "svg",
508 {
518 },
519 [
520 React.createElement("circle", {
521 key: "circle",
522 cx: "12",
524 r: "5",
525 }),
526 React.createElement("line", {
527 key: "line1",
528 x1: "12",
531 y2: "3",
532 }),
533 React.createElement("line", {
534 key: "line2",
535 x1: "12",
538 y2: "23",
539 }),
540 React.createElement("line", {
541 key: "line3",
542 x1: "4.22",
545 y2: "5.64",
546 }),
547 React.createElement("line", {
548 key: "line4",
549 x1: "18.36",
552 y2: "19.78",
553 }),
554 React.createElement("line", {
555 key: "line5",
556 x1: "1",
559 y2: "12",
560 }),
561 React.createElement("line", {
562 key: "line6",
563 x1: "21",
566 y2: "12",
567 }),
568 React.createElement("line", {
569 key: "line7",
570 x1: "4.22",
573 y2: "18.36",
574 }),
575 React.createElement("line", {
576 key: "line8",
577 x1: "18.36",
582 ],
583 )
584 : React.createElement(
585 // こちらが月アイコン (ライトモード時に表示)
586 "svg",
596 strokeLinejoin: "round",
597 },
598 React.createElement("path", {
599 d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z",
600 }),
603 ),
604 // Search Form
605 React.createElement(
606 "form",
607 { onSubmit: handleSubmit, className: "card" },
608 // Account input field (required)
609 React.createElement(
610 "div",
611 { className: "field account-field" },
612 React.createElement(
613 "label",
614 { htmlFor: "targetAccount", className: "label" },
615 "アカウントID(必須)",
616 ),
617 React.createElement(
618 "div",
619 { className: "input-wrapper" },
620 React.createElement("span", { className: "input-prefix" }, "@"),
621 React.createElement("input", {
622 type: "text",
623 id: "targetAccount",
632 ),
633 // Keywords field (optional)
634 React.createElement(
635 "div",
636 { className: "field query-field" },
637 React.createElement(
638 "label",
639 { htmlFor: "queryKeywords", className: "label" },
640 "キーワード(任意)",
641 ),
642 React.createElement("input", {
643 type: "text",
644 id: "queryKeywords",
651 ),
652 // 取得件数フィールド(必須) - キーワードの下に移動
653 React.createElement(
654 "div",
655 { className: "field tweets-count-field" },
656 React.createElement(
657 "label",
658 { htmlFor: "maxTweets", className: "label" },
659 "取得件数(必須)",
660 ),
661 React.createElement("input", {
662 type: "number",
663 id: "maxTweets",
672 ),
673 // Date Range (moved from modal to main form)
674 React.createElement(
675 "div",
676 { className: "field-group" },
677 React.createElement("h3", { className: "group-title" }, "期間"),
678 // Start Date
679 React.createElement(
680 "div",
681 { className: "field-row" },
682 React.createElement("div", { className: "field-label" }, "開始日"),
683 React.createElement(
684 "div",
685 { className: "date-field-group" },
686 // Year
687 React.createElement(
688 "select",
689 {
693 disabled: loading,
694 },
695 React.createElement("option", { value: "" }, "年"),
696 generateYears().map((year) => React.createElement("option", { key: year, value: year }, year)),
697 ),
698 // Month
699 React.createElement(
700 "select",
701 {
705 disabled: loading,
706 },
707 React.createElement("option", { value: "" }, "月"),
708 generateMonths().map((month) =>
709 React.createElement(
710 "option",
711 { key: month, value: month },
715 ),
716 // Day
717 React.createElement(
718 "select",
719 {
723 disabled: loading,
724 },
725 React.createElement("option", { value: "" }, "日"),
726 generateDays().map((day) => React.createElement("option", { key: day, value: day }, day)),
727 ),
728 ),
729 ),
730 // End Date
731 React.createElement(
732 "div",
733 { className: "field-row" },
734 React.createElement("div", { className: "field-label" }, "終了日"),
735 React.createElement(
736 "div",
737 { className: "date-field-group" },
738 // Year
739 React.createElement(
740 "select",
741 {
745 disabled: loading,
746 },
747 React.createElement("option", { value: "" }, "年"),
748 generateYears().map((year) => React.createElement("option", { key: year, value: year }, year)),
749 ),
750 // Month
751 React.createElement(
752 "select",
753 {
757 disabled: loading,
758 },
759 React.createElement("option", { value: "" }, "月"),
760 generateMonths().map((month) =>
761 React.createElement(
762 "option",
763 { key: month, value: month },
767 ),
768 // Day
769 React.createElement(
770 "select",
771 {
775 disabled: loading,
776 },
777 React.createElement("option", { value: "" }, "日"),
778 generateDays().map((day) => React.createElement("option", { key: day, value: day }, day)),
779 ),
780 ),
782 ),
783 // Advanced options button
784 React.createElement(
785 "div",
786 { className: "advanced-options-container" },
787 React.createElement(
788 "button",
789 {
796 // Advanced options modal
797 showAdvanced
798 && React.createElement(
799 "div",
800 { className: "modal-backdrop" },
801 React.createElement(
802 "div",
803 {
805 ref: modalRef,
806 },
807 React.createElement(
808 "div",
809 { className: "modal-header" },
810 React.createElement("h3", {}, "高度な検索オプション"),
811 React.createElement(
812 "button",
813 {
818 ),
819 ),
820 React.createElement(
821 "div",
822 { className: "modal-body" },
823 // Include options (moved into modal)
824 React.createElement(
825 "div",
826 { className: "field-group" },
827 React.createElement(
828 "h3",
829 { className: "group-title" },
830 "含めるオプション",
831 ),
832 React.createElement(
833 "div",
834 { className: "field-row include-options" },
835 React.createElement(
836 "div",
837 { className: "checkbox-field" },
838 React.createElement("input", {
839 type: "checkbox",
840 id: "includeReplies",
843 disabled: loading,
844 }),
845 React.createElement(
846 "label",
847 {
852 ),
853 ),
854 React.createElement(
855 "div",
856 { className: "checkbox-field" },
857 React.createElement("input", {
858 type: "checkbox",
859 id: "includeRetweets",
862 disabled: loading,
863 }),
864 React.createElement(
865 "label",
866 {
874 ),
875 // Engagement Filters
876 React.createElement(
877 "div",
878 { className: "field-group" },
879 React.createElement(
880 "h3",
881 { className: "group-title" },
883 ),
884 // Likes Filter - removed direction dropdown
885 React.createElement(
886 "div",
887 { className: "filter-row" },
888 React.createElement(
889 "label",
890 { className: "filter-label" },
891 "いいね",
892 ),
893 React.createElement(
894 "div",
895 { className: "filter-controls" },
896 React.createElement("input", {
897 type: "number",
898 min: "0",
906 ),
907 // Retweets Filter - removed direction dropdown
908 React.createElement(
909 "div",
910 { className: "filter-row" },
911 React.createElement(
912 "label",
913 { className: "filter-label" },
914 "リツイート",
915 ),
916 React.createElement(
917 "div",
918 { className: "filter-controls" },
919 React.createElement("input", {
920 type: "number",
921 min: "0",
929 ),
930 // Replies Filter - removed direction dropdown
931 React.createElement(
932 "div",
933 { className: "filter-row" },
934 React.createElement(
935 "label",
936 { className: "filter-label" },
937 "返信",
938 ),
939 React.createElement(
940 "div",
941 { className: "filter-controls" },
942 React.createElement("input", {
943 type: "number",
944 min: "0",
952 ),
953 // Bookmarks Filter - removed direction dropdown
954 React.createElement(
955 "div",
956 { className: "filter-row" },
957 React.createElement(
958 "label",
959 { className: "filter-label" },
960 "ブックマーク",
961 ),
962 React.createElement(
963 "div",
964 { className: "filter-controls" },
965 React.createElement("input", {
966 type: "number",
967 min: "0",
975 ),
976 // Views Filter - removed direction dropdown
977 React.createElement(
978 "div",
979 { className: "filter-row" },
980 React.createElement(
981 "label",
982 { className: "filter-label" },
983 "表示回数",
984 ),
985 React.createElement(
986 "div",
987 { className: "filter-controls" },
988 React.createElement("input", {
989 type: "number",
990 min: "0",
999 ),
1000 ),
1001 React.createElement(
1002 "div",
1003 { className: "modal-footer" },
1004 React.createElement(
1005 "button",
1006 {
1016 ),
1017 // Search/Stop Button - Modified to center align
1018 React.createElement(
1019 "div",
1020 { className: "button-row" },
1021 !loading
1022 ? React.createElement(
1023 "button",
1024 { type: "submit", className: "button" },
1025 "検索",
1026 )
1027 : React.createElement(
1028 "button",
1029 {
1038 // Status Messages
1039 (loading || error)
1040 && React.createElement(
1041 "div",
1042 { className: "card status-card" },
1043 loading
1044 && React.createElement("p", { className: "status" }, loadingMessage),
1045 error && React.createElement("p", { className: "error" }, error),
1046 ),
1047 // Results
1048 filteredTweets.length > 0
1049 && React.createElement(
1050 "div",
1051 { className: "card results-card" },
1052 React.createElement(
1053 "div",
1054 { className: "results-header" },
1055 React.createElement(
1056 "div",
1057 { className: "results-summary" },
1058 React.createElement(
1059 "span",
1060 { className: "results-count" },
1061 filteredTweets.length,
1062 ),
1063 React.createElement(
1064 "span",
1065 { className: "results-label" },
1067 ),
1068 // 選択件数表示
1069 selectedTweetCount > 0 && React.createElement(
1070 "span",
1071 { className: "selected-count" },
1073 ),
1074 ),
1075 React.createElement(
1076 "div",
1077 { className: "actions-container" },
1078 // 全選択ボタン
1079 React.createElement(
1080 "button",
1081 {
1087 ),
1088 // 保存ボタン
1089 selectedTweetCount > 0 && React.createElement(
1090 "button",
1091 {
1098 ),
1099 ),
1100 React.createElement(
1101 "div",
1102 { className: "tweet-list" },
1103 filteredTweets.map((tweet) =>
1104 React.createElement(
1105 "div",
1106 { key: tweet.id_str, className: "tweet" },
1107 // チェックボックス
1108 React.createElement(
1109 "div",
1110 { className: "tweet-checkbox" },
1111 React.createElement("input", {
1112 type: "checkbox",
1113 id: `tweet-${tweet.id_str}`,
1118 ),
1119 // Tweet Content Container
1120 React.createElement(
1121 "div",
1122 { className: "tweet-content" },
1123 // Tweet Header
1124 React.createElement(
1125 "div",
1126 { className: "tweet-header" },
1127 React.createElement("img", {
1128 src: tweet.user.profile_image_url_https,
1129 alt: `${tweet.user.name}'s profile`,
1130 className: "avatar",
1131 }),
1132 React.createElement(
1133 "div",
1134 { className: "user-info" },
1135 React.createElement(
1136 "div",
1137 { className: "name-row" },
1138 React.createElement(
1139 "span",
1140 { className: "name" },
1141 tweet.user.name,
1142 ),
1143 React.createElement(
1144 "span",
1145 { className: "username" },
1147 ),
1148 ),
1149 React.createElement(
1150 "div",
1151 { className: "date" },
1164 ),
1165 // Tweet Content
1166 React.createElement(
1167 "p",
1168 { className: "tweet-text" },
1171 // Media
1172 tweet.entities?.media?.length > 0
1173 && React.createElement(
1174 "div",
1175 { className: "media-container" },
1176 tweet.entities.media.map((mediaItem) =>
1177 React.createElement(
1178 "div",
1179 { key: mediaItem.id_str, className: "media-item" },
1180 mediaItem.type === "photo"
1181 || mediaItem.type === "animated_gif"
1182 ? React.createElement(
1183 "a",
1184 {
1187 rel: "noopener noreferrer",
1188 },
1189 React.createElement("img", {
1190 src: mediaItem.media_url_https,
1191 alt: `Media`,
1195 )
1196 : mediaItem.type === "video"
1197 ? React.createElement(
1198 "a",
1199 {
1203 className: "video-link",
1204 },
1205 React.createElement("img", {
1206 src: mediaItem.media_url_https,
1207 alt: `Video preview`,
1209 className: "media",
1210 }),
1211 React.createElement(
1212 "div",
1213 { className: "play-icon" },
1220 ),
1221 // Tweet Footer with Engagement Metrics
1222 React.createElement(
1223 "div",
1224 { className: "tweet-footer" },
1225 React.createElement(
1226 "div",
1227 { className: "metrics" },
1228 React.createElement(
1229 "span",
1230 { className: "metric" },
1232 tweet.reply_count ?? 0,
1233 ),
1234 React.createElement(
1235 "span",
1236 { className: "metric" },
1238 tweet.favorite_count ?? 0,
1239 ),
1240 React.createElement(
1241 "span",
1242 { className: "metric" },
1244 tweet.retweet_count ?? 0,
1245 ),
1246 React.createElement(
1247 "span",
1248 { className: "metric" },
1250 tweet.bookmark_count ?? 0,
1251 ),
1252 React.createElement(
1253 "span",
1254 { className: "metric" },
1257 ),
1258 ),
1259 React.createElement(
1260 "a",
1261 {
1273 ),
1274 // Load more button - Only show if we haven't reached the limit
1275 tweets.length < parseInt(maxTweets) && React.createElement(
1276 "div",
1277 { className: "load-more-container" },
1278 React.createElement(
1279 "button",
1280 {
1292 && currentSearchQuery
1293 && !error
1294 && React.createElement(
1295 "div",
1296 { className: "card empty-card" },
1298 ),
1299 // Footer - Modified text and added link
1300 React.createElement(
1301 "footer",
1302 null,
1303 "developed by ",
1304 React.createElement(
1305 "a",
1306 {
1321 const root = document.getElementById("root");
1322 if (root) {
1323 createRoot(root).render(React.createElement(App));
1324 } else {
1325 console.error("Root element not found");