├── .prettierrc
├── docs
├── issue.png
├── oauth.png
├── repo.png
├── token.png
├── oauth_prod.png
├── oauth_test.png
├── token_premission.png
├── static
│ ├── js
│ │ ├── 820.f55dda59.js.LICENSE.txt
│ │ └── lib-react.80f573d9.js.LICENSE.txt
│ └── css
│ │ └── 820.b3abc803.css
└── index.html
├── .prettierignore
├── src
├── index.tsx
├── i18n
│ ├── index.ts
│ └── locales
│ │ ├── zh.json
│ │ └── en.json
├── types
│ └── global.d.ts
├── components
│ ├── Label.tsx
│ ├── AnimatedCard.tsx
│ ├── LanguageSwitcher.tsx
│ ├── common
│ │ ├── IssueLayout.tsx
│ │ └── Spotlight.tsx
│ ├── SkeletonCard.tsx
│ ├── CommentInput.tsx
│ ├── Issue.tsx
│ ├── About.tsx
│ ├── Egg.tsx
│ ├── Toolbar.tsx
│ └── Interaction.tsx
├── utils
│ ├── cache.ts
│ ├── request.ts
│ └── index.ts
├── config
│ └── index.ts
├── gwitter.ts
├── AuthWindow.tsx
├── hooks
│ └── useAuth.tsx
├── lib
│ └── collapse.js
└── App.tsx
├── .gitignore
├── demo
├── npm-demo
│ ├── vite.config.ts
│ ├── tsconfig.node.json
│ ├── src
│ │ ├── main.tsx
│ │ ├── index.css
│ │ ├── config
│ │ │ └── gwitter.config.ts
│ │ ├── App.css
│ │ └── App.tsx
│ ├── index.html
│ ├── package.json
│ └── tsconfig.json
├── README.md
└── umd-demo
│ └── index.html
├── .npmignore
├── tsconfig.json
├── eslint.config.mjs
├── LICENSE
├── rsbuild.config.mjs
├── package.json
├── public
└── index.html
├── rollup.config.js
├── README.zh_CN.md
└── README.md
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/docs/issue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonAKing/Gwitter/HEAD/docs/issue.png
--------------------------------------------------------------------------------
/docs/oauth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonAKing/Gwitter/HEAD/docs/oauth.png
--------------------------------------------------------------------------------
/docs/repo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonAKing/Gwitter/HEAD/docs/repo.png
--------------------------------------------------------------------------------
/docs/token.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonAKing/Gwitter/HEAD/docs/token.png
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Lock files
2 | package-lock.json
3 | pnpm-lock.yaml
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/docs/oauth_prod.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonAKing/Gwitter/HEAD/docs/oauth_prod.png
--------------------------------------------------------------------------------
/docs/oauth_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonAKing/Gwitter/HEAD/docs/oauth_test.png
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import gwitter from './gwitter';
2 | import './i18n';
3 |
4 | gwitter();
5 |
--------------------------------------------------------------------------------
/docs/token_premission.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonAKing/Gwitter/HEAD/docs/token_premission.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Local
2 | .DS_Store
3 | *.local
4 | *.log*
5 |
6 | # Dist
7 | node_modules
8 | dist/
9 |
10 | # IDE
11 | .vscode/*
12 | !.vscode/extensions.json
13 | .idea
14 |
--------------------------------------------------------------------------------
/demo/npm-demo/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | server: {
7 | port: 3000,
8 | open: true
9 | }
10 | })
--------------------------------------------------------------------------------
/demo/npm-demo/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
--------------------------------------------------------------------------------
/demo/npm-demo/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
--------------------------------------------------------------------------------
/docs/static/js/820.f55dda59.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /** @license React v16.13.1
2 | * react-is.production.min.js
3 | *
4 | * Copyright (c) Facebook, Inc. and its affiliates.
5 | *
6 | * This source code is licensed under the MIT license found in the
7 | * LICENSE file in the root directory of this source tree.
8 | */
--------------------------------------------------------------------------------
/demo/npm-demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Gwitter NPM Demo
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/npm-demo/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html {
6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
7 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
8 | sans-serif;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | }
12 |
13 | body {
14 | margin: 0;
15 | padding: 0;
16 | background-color: #f5f5f5;
17 | color: #333;
18 | line-height: 1.6;
19 | }
20 |
21 | #root {
22 | min-height: 100vh;
23 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Development files
2 | src/
3 | public/
4 | docs/
5 | .git/
6 | .github/
7 | node_modules/
8 | .rsbuild/
9 |
10 | # Config files
11 | rsbuild.config.mjs
12 | rollup.config.js
13 | tsconfig.json
14 | eslint.config.mjs
15 | .prettierrc
16 | .prettierignore
17 | .gitignore
18 |
19 | # Build scripts
20 | pnpm-lock.yaml
21 | package-lock.json
22 | yarn.lock
23 |
24 | # Test files
25 | test-lib.html
26 | **/*.test.*
27 | **/*.spec.*
28 |
29 | # Development tools
30 | .vscode/
31 | .idea/
32 | *.log
33 | *.tmp
34 | *.temp
35 |
36 | # OS files
37 | .DS_Store
38 | Thumbs.db
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import LanguageDetector from 'i18next-browser-languagedetector';
3 | import { initReactI18next } from 'react-i18next';
4 | import en from './locales/en.json';
5 | import zh from './locales/zh.json';
6 |
7 | i18n
8 | .use(LanguageDetector)
9 | .use(initReactI18next)
10 | .init({
11 | resources: {
12 | en: { translation: en },
13 | zh: { translation: zh },
14 | },
15 | fallbackLng: 'en',
16 | interpolation: {
17 | escapeValue: false,
18 | },
19 | });
20 |
21 | export default i18n;
22 |
--------------------------------------------------------------------------------
/demo/npm-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gwitter-npm-demo",
3 | "version": "1.0.0",
4 | "description": "Demo for using Gwitter with NPM",
5 | "private": true,
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "gwitter": "^1.1.0",
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0"
15 | },
16 | "devDependencies": {
17 | "@types/react": "^18.2.0",
18 | "@types/react-dom": "^18.2.0",
19 | "@vitejs/plugin-react": "^4.0.0",
20 | "typescript": "^5.0.0",
21 | "vite": "^4.4.0"
22 | }
23 | }
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | export interface GwitterConfig {
2 | request?: {
3 | token?: string;
4 | clientID?: string;
5 | clientSecret?: string;
6 | pageSize?: number;
7 | autoProxy?: string;
8 | owner?: string;
9 | repo?: string;
10 | };
11 | app?: {
12 | onlyShowOwner?: boolean;
13 | enableRepoSwitcher?: boolean;
14 | enableAbout?: boolean;
15 | enableEgg?: boolean;
16 | };
17 | }
18 |
19 | export interface GwitterOptions {
20 | container?: HTMLElement;
21 | config?: GwitterConfig;
22 | }
23 |
24 | declare global {
25 | interface Window {
26 | gwitter?: (options?: GwitterOptions) => void;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2016",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": false,
17 | "jsx": "react-jsx",
18 | "declaration": true,
19 | "declarationMap": true,
20 | "outDir": "dist"
21 | },
22 | "include": ["src"]
23 | }
--------------------------------------------------------------------------------
/demo/npm-demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { fixupConfigRules } from '@eslint/compat';
2 | import js from '@eslint/js';
3 | import reactHooks from 'eslint-plugin-react-hooks';
4 | import reactJsx from 'eslint-plugin-react/configs/jsx-runtime.js';
5 | import react from 'eslint-plugin-react/configs/recommended.js';
6 | import globals from 'globals';
7 |
8 | export default [
9 | { languageOptions: { globals: globals.browser } },
10 | js.configs.recommended,
11 | ...fixupConfigRules([
12 | {
13 | ...react,
14 | settings: {
15 | react: { version: 'detect' },
16 | },
17 | },
18 | reactJsx,
19 | ]),
20 | {
21 | plugins: {
22 | 'react-hooks': reactHooks,
23 | },
24 | rules: {
25 | ...reactHooks.configs.recommended.rules,
26 | },
27 | },
28 | { ignores: ['dist/'] },
29 | ];
30 |
--------------------------------------------------------------------------------
/src/components/Label.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { getColorByBgColor } from '../utils';
3 |
4 | interface LabelProps {
5 | name: string;
6 | color: string;
7 | style?: React.CSSProperties;
8 | }
9 |
10 | const LabelContainer = styled.span<{ bgColor: string }>`
11 | display: inline-block;
12 | line-height: 1;
13 | padding: 5px 6px;
14 | font-size: 0.9em;
15 | font-weight: 600;
16 | border-radius: 3px;
17 | box-shadow: inset 0 -1px 0 rgba(27, 31, 35, 0.12);
18 | background-color: #${(props) => props.bgColor};
19 | color: ${(props) => getColorByBgColor(props.bgColor)};
20 | `;
21 |
22 | const Label: React.FC = ({ name, color, style }) => {
23 | return (
24 |
25 | {name}
26 |
27 | );
28 | };
29 |
30 | export default Label;
31 |
--------------------------------------------------------------------------------
/src/utils/cache.ts:
--------------------------------------------------------------------------------
1 | const LAST_REPO_KEY = 'gwitter_last_repo';
2 |
3 | export const saveLastRepo = (owner: string, repo: string) => {
4 | try {
5 | const repoData = { owner, repo };
6 | localStorage.setItem(LAST_REPO_KEY, JSON.stringify(repoData));
7 | console.log('Saved last repo:', `${owner}/${repo}`);
8 | } catch (error) {
9 | console.warn('Failed to save last repo:', error);
10 | }
11 | };
12 |
13 | export const loadLastRepo = (): { owner: string; repo: string } | null => {
14 | try {
15 | const cached = localStorage.getItem(LAST_REPO_KEY);
16 | if (cached) {
17 | const repoData = JSON.parse(cached);
18 | console.log('Loaded last repo:', `${repoData.owner}/${repoData.repo}`);
19 | return repoData;
20 | }
21 | return null;
22 | } catch (error) {
23 | console.warn('Failed to load last repo:', error);
24 | return null;
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/AnimatedCard.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion';
2 | import { ReactNode } from 'react';
3 |
4 | interface AnimatedCardProps {
5 | children: ReactNode;
6 | id: string;
7 | }
8 |
9 | const AnimatedCard = ({ children, id }: AnimatedCardProps) => {
10 | return (
11 |
39 | {children}
40 |
41 | );
42 | };
43 |
44 | export default AnimatedCard;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 simonma
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import { GwitterConfig } from '../types/global';
2 |
3 | let config = {
4 | request: {
5 | token:
6 | 'g?i?t?h?u?b?_?p?a?t?_?1?1?A?H?V?6?E?W?Q?0?M?f?C?S?r?0?4?K?A?j?1?F?_?3?7?n?4?U?y?u?S?m?d?z?i?t?D?s?w?i?s?i?u?a?g?N?b?a?k?V?n?L?I?7?U?W?s?s?h?n?K?p?s?H?S?D?S?4?D?K?O?Q?Q?J?S?S?x?q?z?Z?X?M',
7 | clientID: '56af6ab05592f0a2d399',
8 | clientSecret: '5d7e71a1b6130001e84956420ca5b88bc45b7d3c',
9 | pageSize: 6,
10 | autoProxy:
11 | 'https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token',
12 | owner: 'SimonAKing',
13 | repo: 'weibo',
14 | },
15 |
16 | app: {
17 | onlyShowOwner: false,
18 | enableRepoSwitcher: true,
19 | enableAbout: false,
20 | enableEgg: false,
21 | },
22 | };
23 |
24 | export function setConfig(newConfig: GwitterConfig) {
25 | if (newConfig.request) {
26 | config.request = {
27 | ...config.request,
28 | ...newConfig.request,
29 | };
30 | }
31 |
32 | if (newConfig.app) {
33 | config.app = {
34 | ...config.app,
35 | ...newConfig.app,
36 | };
37 | }
38 | }
39 |
40 | export default config;
41 |
--------------------------------------------------------------------------------
/docs/static/js/lib-react.80f573d9.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /**
2 | * @license React
3 | * react-dom.production.min.js
4 | *
5 | * Copyright (c) Facebook, Inc. and its affiliates.
6 | *
7 | * This source code is licensed under the MIT license found in the
8 | * LICENSE file in the root directory of this source tree.
9 | */
10 |
11 | /**
12 | * @license React
13 | * react-jsx-runtime.production.min.js
14 | *
15 | * Copyright (c) Facebook, Inc. and its affiliates.
16 | *
17 | * This source code is licensed under the MIT license found in the
18 | * LICENSE file in the root directory of this source tree.
19 | */
20 |
21 | /**
22 | * @license React
23 | * react.production.min.js
24 | *
25 | * Copyright (c) Facebook, Inc. and its affiliates.
26 | *
27 | * This source code is licensed under the MIT license found in the
28 | * LICENSE file in the root directory of this source tree.
29 | */
30 |
31 | /**
32 | * @license React
33 | * scheduler.production.min.js
34 | *
35 | * Copyright (c) Facebook, Inc. and its affiliates.
36 | *
37 | * This source code is licensed under the MIT license found in the
38 | * LICENSE file in the root directory of this source tree.
39 | */
--------------------------------------------------------------------------------
/demo/npm-demo/src/config/gwitter.config.ts:
--------------------------------------------------------------------------------
1 | // Gwitter Configuration
2 |
3 | export interface GwitterConfig {
4 | request: {
5 | token: string;
6 | clientID: string;
7 | clientSecret: string;
8 | owner: string;
9 | repo: string;
10 | pageSize?: number;
11 | autoProxy?: string;
12 | };
13 | app?: {
14 | onlyShowOwner?: boolean;
15 | enableRepoSwitcher?: boolean;
16 | enableAbout?: boolean;
17 | enableEgg?: boolean;
18 | };
19 | }
20 |
21 | // Default configuration
22 | export const config: GwitterConfig = {
23 | request: {
24 | token:
25 | 'g?i?t?h?u?b?_?p?a?t?_?1?1?A?H?V?6?E?W?Q?0?M?f?C?S?r?0?4?K?A?j?1?F?_?3?7?n?4?U?y?u?S?m?d?z?i?t?D?s?w?i?s?i?u?a?g?N?b?a?k?V?n?L?I?7?U?W?s?s?h?n?K?p?s?H?S?D?S?4?D?K?O?Q?Q?J?S?S?x?q?z?Z?X?M',
26 | clientID: '56af6ab05592f0a2d399',
27 | clientSecret: '5d7e71a1b6130001e84956420ca5b88bc45b7d3c',
28 | pageSize: 6,
29 | autoProxy:
30 | 'https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token',
31 | owner: 'SimonAKing',
32 | repo: 'weibo',
33 | },
34 | app: {
35 | onlyShowOwner: true,
36 | enableAbout: true,
37 | enableRepoSwitcher: false,
38 | enableEgg: true,
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/src/gwitter.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App';
4 | import AuthWindow from './AuthWindow';
5 | import { setConfig } from './config';
6 | import './i18n';
7 | import type { GwitterOptions } from './types/global';
8 | import { parseUrl } from './utils';
9 |
10 | let gwitterInstance: ReactDOM.Root | null = null;
11 |
12 | function renderGwitter(container: HTMLElement) {
13 | const params = parseUrl();
14 | let component = App;
15 | if (params.code) {
16 | component = AuthWindow;
17 | }
18 |
19 | if (gwitterInstance) {
20 | gwitterInstance.unmount();
21 | }
22 |
23 | const root = ReactDOM.createRoot(container);
24 | root.render(React.createElement(component));
25 |
26 | gwitterInstance = root;
27 |
28 | return root;
29 | }
30 |
31 | function gwitter(options: GwitterOptions = {}) {
32 | const container = options.container || document.getElementById('gwitter');
33 | if (!container) {
34 | console.error('Gwitter: Container element not found');
35 | return;
36 | }
37 |
38 | setConfig(options.config || {});
39 |
40 | return renderGwitter(container);
41 | }
42 |
43 | if (typeof window !== 'undefined') {
44 | window.gwitter = gwitter;
45 | }
46 |
47 | export default gwitter;
48 |
--------------------------------------------------------------------------------
/rsbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@rsbuild/core';
2 | import { pluginLess } from '@rsbuild/plugin-less';
3 | import { pluginReact } from '@rsbuild/plugin-react';
4 |
5 | export default defineConfig({
6 | html: {
7 | template: './public/index.html',
8 | },
9 | plugins: [pluginReact(), pluginLess()],
10 | module: {
11 | rules: [
12 | {
13 | test: /\.ts$/,
14 | exclude: [/node_modules/],
15 | loader: 'builtin:swc-loader',
16 | options: {
17 | jsc: {
18 | parser: {
19 | syntax: 'typescript',
20 | },
21 | },
22 | },
23 | type: 'javascript/auto',
24 | },
25 | {
26 | test: /\.jsx$/,
27 | use: {
28 | loader: 'builtin:swc-loader',
29 | options: {
30 | jsc: {
31 | parser: {
32 | syntax: 'ecmascript',
33 | jsx: true,
34 | },
35 | },
36 | },
37 | },
38 | type: 'javascript/auto',
39 | },
40 | {
41 | test: /\.tsx$/,
42 | use: {
43 | loader: 'builtin:swc-loader',
44 | options: {
45 | jsc: {
46 | parser: {
47 | syntax: 'typescript',
48 | tsx: true,
49 | },
50 | },
51 | },
52 | },
53 | type: 'javascript/auto',
54 | },
55 | ],
56 | },
57 | output: {
58 | assetPrefix: 'https://simonaking.com/Gwitter/',
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/src/components/LanguageSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { useTranslation } from 'react-i18next';
3 |
4 | const SwitcherContainer = styled.div`
5 | display: flex;
6 | align-items: center;
7 | gap: 8px;
8 | `;
9 |
10 | const LanguageButton = styled.button<{ isActive: boolean }>`
11 | background: ${(props) => (props.isActive ? '#1da1f2' : 'transparent')};
12 | color: ${(props) => (props.isActive ? 'white' : '#657786')};
13 | border: 1px solid ${(props) => (props.isActive ? '#1da1f2' : '#e1e8ed')};
14 | padding: 4px 8px;
15 | border-radius: 12px;
16 | cursor: pointer;
17 | font-size: 12px;
18 | font-weight: 500;
19 | transition: all 0.2s;
20 | min-width: 32px;
21 |
22 | &:hover {
23 | background: ${(props) => (props.isActive ? '#1991db' : '#f7f9fa')};
24 | border-color: ${(props) => (props.isActive ? '#1991db' : '#d1d9e0')};
25 | }
26 |
27 | &:active {
28 | transform: scale(0.95);
29 | }
30 | `;
31 |
32 | const LanguageSwitcher = () => {
33 | const { i18n } = useTranslation();
34 |
35 | const changeLanguage = (lng: string) => {
36 | i18n.changeLanguage(lng);
37 | };
38 |
39 | return (
40 |
41 | changeLanguage('zh')}
44 | >
45 | 中
46 |
47 | changeLanguage('en')}
50 | >
51 | EN
52 |
53 |
54 | );
55 | };
56 |
57 | export default LanguageSwitcher;
58 |
--------------------------------------------------------------------------------
/src/components/common/IssueLayout.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const IssueContainer = styled.div`
4 | position: relative;
5 | margin: 0.5em 0;
6 | display: flex;
7 | border-radius: 10px;
8 | `;
9 |
10 | export const IssueContent = styled.div`
11 | flex: 1 1;
12 | padding: 16px 20px 0px;
13 | margin: 6px;
14 | overflow: auto;
15 | background: hsla(0, 0%, 100%, 0.8);
16 | border: 0.5px solid #f1f1f1;
17 | border-radius: 10px;
18 | box-shadow: 0 0.1em 0.2em 0 rgba(234, 234, 234, 0.8);
19 | transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
20 | position: relative;
21 | overflow: hidden;
22 | font-size: 15px;
23 | z-index: 2;
24 |
25 | /* border: 1px solid rgb(212, 212, 216); */
26 | /* box-shadow:
27 | rgba(0, 0, 0, 0) 0px 0px 0px 0px,
28 | rgba(0, 0, 0, 0) 0px 0px 0px 0px,
29 | rgba(0, 0, 0, 0.15) 2px 0px 8px 0px; */
30 |
31 | /* &:hover { */
32 | /* box-shadow: 0 0.2em 0.3em 0.1em rgba(200, 200, 200, 0.4); */
33 | /* transform: translateY(-1px); */
34 | /* } */
35 | `;
36 |
37 | export const IssueHeader = styled.div`
38 | margin-bottom: 0.7em;
39 | font-size: 1em;
40 | position: relative;
41 | display: flex;
42 | align-items: center;
43 | flex-wrap: wrap;
44 | /* gap: 0.2em; */
45 | `;
46 |
47 | export const IssueBody = styled.div`
48 | color: #333;
49 | &.markdown-body {
50 | font-size: 1em;
51 | letter-spacing: 0.2px;
52 | word-wrap: break-word;
53 | background-color: transparent;
54 | /* background: hsla(0, 0%, 100%, 0.8); */
55 | ol {
56 | list-style: decimal !important;
57 | }
58 | ul {
59 | list-style: circle !important;
60 | }
61 | }
62 | `;
63 |
64 | export const IssueFooter = styled.div`
65 | position: relative;
66 | margin-top: 0.8em;
67 | font-size: 1em;
68 | user-select: none;
69 | `;
70 |
--------------------------------------------------------------------------------
/src/i18n/locales/zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth": {
3 | "authorizing": "正在授权中...",
4 | "login": "登录 GitHub",
5 | "logout": "退出",
6 | "loading": "加载中..."
7 | },
8 | "toolbar": {
9 | "repo": "仓库",
10 | "apply": "应用",
11 | "repoPlaceholder": "用户名/仓库名",
12 | "invalidRepo": "请输入有效的仓库地址(用户名/仓库名)",
13 | "repoNotFound": "仓库未找到或为私有仓库"
14 | },
15 | "about": {
16 | "title": "Details & Summary",
17 | "gwitter": {
18 | "title": "🎉 关于 Gwitter",
19 | "description": "这是一个基于 GitHub Issues 构建的轻量级微博应用。这里记录着我对技术的思考、对生活的感悟,以及一些有趣的发现,欢迎一起交流。"
20 | },
21 | "content": {
22 | "title": "✨ 关于内容",
23 | "categories": "共有{{count}}个分类:"
24 | },
25 | "subscription": {
26 | "title": "🔊 关于订阅",
27 | "watch": "Watch",
28 | "join": "Join",
29 | "repo": "Gwitter 仓库",
30 | "wechat": "微信群",
31 | "telegram": "TG 频道"
32 | }
33 | },
34 | "egg": {
35 | "message": "感谢浏览!",
36 | "hope": "期待再次相见",
37 | "comment": "// TODO: 添加更多有趣内容",
38 | "runCode": "运行代码"
39 | },
40 | "interaction": {
41 | "like": "点赞",
42 | "liked": "已点赞",
43 | "comment": "评论",
44 | "comments": "条评论",
45 | "loginRequired": "请先登录后再进行操作",
46 | "loginToLike": "登录后点赞",
47 | "loginToComment": "登录后评论"
48 | },
49 | "comments": {
50 | "adding": "评论中...",
51 | "saving": "保存中...",
52 | "loading": "加载评论中...",
53 | "empty": "暂无评论",
54 | "add": "评论",
55 | "edit": "编辑",
56 | "delete": "删除",
57 | "cancel": "取消",
58 | "save": "保存",
59 | "placeholder": "写下你的评论...",
60 | "confirmDelete": "确定要删除这条评论吗?",
61 | "confirmDeleteTitle": "删除评论",
62 | "confirmDeleteMessage": "确定要删除这条评论吗?删除后将无法恢复。",
63 | "deleting": "删除中...",
64 | "addSuccess": "评论添加成功",
65 | "updateSuccess": "评论更新成功",
66 | "deleteSuccess": "评论删除成功",
67 | "addFailed": "评论添加失败",
68 | "updateFailed": "评论更新失败",
69 | "deleteFailed": "评论删除失败",
70 | "more": "更多操作",
71 | "like": "点赞",
72 | "liked": "已点赞",
73 | "likes": "个赞"
74 | }
75 | }
--------------------------------------------------------------------------------
/src/components/SkeletonCard.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import {
3 | IssueBody,
4 | IssueContent,
5 | IssueFooter,
6 | IssueHeader,
7 | } from './common/IssueLayout';
8 |
9 | const SkeletonBase = styled.div`
10 | background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
11 | background-size: 200px 100%;
12 | animation: skeleton-loading 3s infinite;
13 | border-radius: 4px;
14 | margin-bottom: 8px;
15 |
16 | @keyframes skeleton-loading {
17 | 0% {
18 | background-position: -200px 0;
19 | }
20 | 100% {
21 | background-position: calc(200px + 100%) 0;
22 | }
23 | }
24 | `;
25 |
26 | const SkeletonAvatar = styled(SkeletonBase)`
27 | width: 2em;
28 | height: 2em;
29 | border-radius: 50%;
30 | margin-right: 0.5em;
31 | margin-bottom: 0;
32 | display: inline-flex;
33 | align-self: center;
34 | `;
35 |
36 | const SkeletonUsername = styled(SkeletonBase)`
37 | width: 120px;
38 | height: 20px;
39 | display: inline-flex;
40 | align-self: center;
41 | margin-bottom: 0;
42 | `;
43 |
44 | const SkeletonDate = styled(SkeletonBase)`
45 | width: 80px;
46 | height: 16px;
47 | display: inline-flex;
48 | align-self: center;
49 | margin-bottom: 0;
50 | margin-left: 20px;
51 | `;
52 |
53 | const SkeletonLabel = styled(SkeletonBase)`
54 | width: 60px;
55 | height: 24px;
56 | position: absolute;
57 | right: 0;
58 | top: 0;
59 | `;
60 |
61 | const SkeletonLine = styled(SkeletonBase)<{ width: string }>`
62 | height: 16px;
63 | margin-top: 12px;
64 | width: ${(props) => props.width};
65 | `;
66 |
67 | const SkeletonInteractions = styled(SkeletonBase)`
68 | width: 100px;
69 | height: 20px;
70 | margin: 16px 0px;
71 | `;
72 |
73 | export const SkeletonContainer = styled.div`
74 | position: relative;
75 | margin-bottom: 0.5em;
76 | display: flex;
77 | border-radius: 10px;
78 | `;
79 |
80 | export const SkeletonCard = () => (
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | );
101 |
102 | export default SkeletonCard;
103 |
--------------------------------------------------------------------------------
/src/i18n/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth": {
3 | "authorizing": "Authorizing...",
4 | "login": "Login with GitHub",
5 | "logout": "Logout",
6 | "loading": "Loading..."
7 | },
8 | "toolbar": {
9 | "repo": "Repo",
10 | "apply": "Apply",
11 | "repoPlaceholder": "owner/repo",
12 | "invalidRepo": "Please enter a valid repository (owner/repo)",
13 | "repoNotFound": "Repository not found or private"
14 | },
15 | "about": {
16 | "title": "Details & Summary",
17 | "gwitter": {
18 | "title": "🎉 About Gwitter",
19 | "description": "This is a lightweight microblogging application built on GitHub Issues. Here I record my thoughts on technology, insights into life, and some interesting discoveries. Welcome to join the discussion."
20 | },
21 | "content": {
22 | "title": "✨ About Content",
23 | "categories": "There are {{count}} categories:"
24 | },
25 | "subscription": {
26 | "title": "🔊 About Subscription",
27 | "watch": "Watch",
28 | "join": "Join",
29 | "repo": "Gwitter Repository",
30 | "wechat": "WeChat Group",
31 | "telegram": "TG Channel"
32 | }
33 | },
34 | "egg": {
35 | "message": "Thanks for visiting!",
36 | "hope": "Hope to see you again",
37 | "comment": "// TODO: Add more interesting content",
38 | "runCode": "Run Code"
39 | },
40 | "interaction": {
41 | "like": "Like",
42 | "liked": "Liked",
43 | "comment": "Comment",
44 | "comments": "comments",
45 | "loginRequired": "Please login first",
46 | "loginToLike": "Login to like",
47 | "loginToComment": "Login to comment"
48 | },
49 | "comments": {
50 | "adding": "Commenting...",
51 | "saving": "Saving...",
52 | "loading": "Loading comments...",
53 | "empty": "No comments yet",
54 | "add": "Comment",
55 | "edit": "Edit",
56 | "delete": "Delete",
57 | "cancel": "Cancel",
58 | "save": "Save",
59 | "placeholder": "Write your comment...",
60 | "confirmDelete": "Are you sure you want to delete this comment?",
61 | "confirmDeleteTitle": "Delete Comment",
62 | "confirmDeleteMessage": "Are you sure you want to delete this comment? This action cannot be undone.",
63 | "deleting": "Deleting...",
64 | "addSuccess": "Comment added successfully",
65 | "updateSuccess": "Comment updated successfully",
66 | "deleteSuccess": "Comment deleted successfully",
67 | "addFailed": "Failed to add comment",
68 | "updateFailed": "Failed to update comment",
69 | "deleteFailed": "Failed to delete comment",
70 | "more": "More actions",
71 | "like": "Like",
72 | "liked": "Liked",
73 | "likes": "likes"
74 | }
75 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gwitter",
3 | "version": "1.1.0",
4 | "description": "Turn GitHub Issues into your personal microblog platform",
5 | "main": "dist/gwitter.min.js",
6 | "module": "dist/gwitter.esm.js",
7 | "types": "dist/gwitter.d.ts",
8 | "files": [
9 | "dist",
10 | "README.md",
11 | "LICENSE"
12 | ],
13 | "keywords": [
14 | "issues",
15 | "github",
16 | "twitter",
17 | "blogging"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/SimonAKing/Gwitter"
22 | },
23 | "license": "MIT",
24 | "publishConfig": {
25 | "access": "public"
26 | },
27 | "private": false,
28 | "type": "module",
29 | "scripts": {
30 | "build": "rsbuild build",
31 | "build:lib": "rollup -c rollup.config.js",
32 | "dev": "rsbuild dev --open",
33 | "format": "prettier --write .",
34 | "lint": "eslint .",
35 | "preview": "rsbuild preview",
36 | "prepublishOnly": "pnpm run build:lib",
37 | "publish:npm": "npm publish"
38 | },
39 | "dependencies": {
40 | "@emotion/react": "^11.11.3",
41 | "@emotion/styled": "^11.11.0",
42 | "@number-flow/react": "^0.5.9",
43 | "@types/lodash": "^4.17.17",
44 | "@types/react-flip-move": "^2.9.12",
45 | "@types/react-router-dom": "^5.3.3",
46 | "axios": "^1.9.0",
47 | "balloons-js": "^0.0.3",
48 | "date-fns": "^4.1.0",
49 | "framer-motion": "^12.15.0",
50 | "github-markdown-css": "^5.8.1",
51 | "i18next": "^23.7.16",
52 | "i18next-browser-languagedetector": "^8.1.0",
53 | "lodash": "^4.17.21",
54 | "react": "^18.2.0",
55 | "react-dom": "^18.2.0",
56 | "react-flip-move": "^3.0.5",
57 | "react-i18next": "^14.0.5",
58 | "react-router-dom": "^6.21.3"
59 | },
60 | "devDependencies": {
61 | "@eslint/compat": "^1.2.7",
62 | "@eslint/js": "^9.23.0",
63 | "@rollup/plugin-commonjs": "^28.0.6",
64 | "@rollup/plugin-json": "^6.1.0",
65 | "@rollup/plugin-node-resolve": "^16.0.1",
66 | "@rollup/plugin-replace": "^6.0.2",
67 | "@rollup/plugin-terser": "^0.4.4",
68 | "@rollup/plugin-typescript": "^12.1.4",
69 | "@rsbuild/core": "^1.3.15",
70 | "@rsbuild/plugin-less": "^1.2.4",
71 | "@rsbuild/plugin-react": "^1.3.0",
72 | "@types/node": "^24.0.7",
73 | "@types/react": "^18.2.48",
74 | "@types/react-dom": "^18.2.18",
75 | "eslint": "^9.23.0",
76 | "eslint-plugin-react": "^7.33.2",
77 | "eslint-plugin-react-hooks": "^4.6.0",
78 | "globals": "^16.0.0",
79 | "less": "^4.3.0",
80 | "postcss-less": "^6.0.0",
81 | "prettier": "^3.5.3",
82 | "rollup": "^4.45.0",
83 | "rollup-plugin-postcss": "^4.0.2",
84 | "tslib": "^2.8.1"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/demo/npm-demo/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | /* text-align: center; */
3 | max-width: 1200px;
4 | margin: 0 auto;
5 | padding: 20px;
6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
7 | }
8 |
9 | .App-header {
10 | background-color: #f8f9fa;
11 | padding: 20px;
12 | border-radius: 8px;
13 | margin-bottom: 30px;
14 | }
15 |
16 | .App-header h1 {
17 | color: #2c3e50;
18 | margin: 0 0 10px 0;
19 | font-size: 2.5em;
20 | }
21 |
22 | .App-header p {
23 | color: #666;
24 | margin: 10px 0;
25 | font-size: 1.1em;
26 | line-height: 1.6;
27 | }
28 |
29 | .App-header code {
30 | background-color: #e9ecef;
31 | padding: 2px 6px;
32 | border-radius: 4px;
33 | font-family: 'Monaco', 'Menlo', monospace;
34 | }
35 |
36 | .App-main {
37 | min-height: 400px;
38 | margin: 30px 0;
39 | }
40 |
41 | .demo-info {
42 | background-color: #f8f9fa;
43 | border: 1px solid #e9ecef;
44 | border-radius: 8px;
45 | padding: 20px;
46 | margin: 20px 0;
47 | }
48 |
49 | .demo-info h2 {
50 | margin: 0 0 16px 0;
51 | color: #2c3e50;
52 | font-size: 1.3em;
53 | }
54 |
55 | .config-display {
56 | display: grid;
57 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
58 | gap: 12px;
59 | }
60 |
61 | .config-item {
62 | background-color: white;
63 | padding: 12px;
64 | border-radius: 6px;
65 | border: 1px solid #e9ecef;
66 | font-size: 0.9em;
67 | }
68 |
69 | .config-item strong {
70 | color: #495057;
71 | display: block;
72 | margin-bottom: 4px;
73 | }
74 |
75 | .gwitter-demo-container {
76 | margin: 20px 0;
77 | border: 1px solid #e1e8ed;
78 | border-radius: 8px;
79 | min-height: 400px;
80 | background-color: #fff;
81 | position: relative;
82 | }
83 |
84 | #gwitter-container {
85 | min-height: 400px;
86 | }
87 |
88 | .App-footer {
89 | background-color: #f8f9fa;
90 | padding: 20px;
91 | border-radius: 8px;
92 | margin-top: 30px;
93 | border-top: 1px solid #e1e8ed;
94 | text-align: center;
95 | }
96 |
97 | .footer-links {
98 | display: flex;
99 | justify-content: center;
100 | gap: 20px;
101 | margin-bottom: 12px;
102 | flex-wrap: wrap;
103 | }
104 |
105 | .footer-links a {
106 | color: #1976d2;
107 | text-decoration: none;
108 | font-weight: 500;
109 | padding: 8px 16px;
110 | border-radius: 6px;
111 | background-color: white;
112 | border: 1px solid #e9ecef;
113 | transition: all 0.3s ease;
114 | }
115 |
116 | .footer-links a:hover {
117 | background-color: #1976d2;
118 | color: white;
119 | transform: translateY(-2px);
120 | box-shadow: 0 2px 8px rgba(25, 118, 210, 0.3);
121 | }
122 |
123 | .footer-note {
124 | margin: 0;
125 | color: #666;
126 | font-size: 0.9em;
127 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Gwitter
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
54 |
55 |
56 |
57 |
63 |
64 |