├── .gitignore ├── frontend ├── package.json.md5 ├── .prettierignore ├── src │ ├── vite-env.d.ts │ ├── tailwind.css │ ├── assets │ │ ├── images │ │ │ └── logo-universal.png │ │ └── fonts │ │ │ ├── nunito-v16-latin-regular.woff2 │ │ │ └── OFL.txt │ ├── main.tsx │ ├── css │ │ └── mixins.scss │ ├── style.css │ ├── components │ │ ├── GitInstallSidebar.tsx │ │ ├── ContributionCalendar.module.scss │ │ ├── RemoteRepoModal.tsx │ │ ├── CharacterSelector.tsx │ │ ├── GitPathSettings.tsx │ │ ├── LoginModal.tsx │ │ └── CalendarControls.tsx │ ├── App.tsx │ ├── App.css │ ├── i18n.tsx │ └── data │ │ └── characterPatterns.ts ├── commitlint.config.js ├── postcss.config.cjs ├── tailwind.config.js ├── .prettierrc.json ├── tsconfig.node.json ├── vite.config.ts ├── index.html ├── wailsjs │ ├── runtime │ │ ├── package.json │ │ ├── runtime.js │ │ └── runtime.d.ts │ └── go │ │ ├── main │ │ ├── App.d.ts │ │ └── App.js │ │ └── models.ts ├── tsconfig.json ├── eslint.config.js └── package.json ├── docs ├── images │ ├── app.png │ ├── cat.png │ ├── app_zh.png │ ├── appnew.png │ ├── cailg.png │ ├── darkcat.jpg │ ├── darkhw.png │ ├── token1.png │ ├── token2.png │ ├── token3.png │ ├── token4.png │ ├── appnew_en.png │ ├── darkandroid.png │ └── privatesetting.png ├── githubtoken.md └── githubtoken_en.md ├── .husky ├── pre-commit └── commit-msg ├── cmd_nonwindows.go ├── cmd_windows.go ├── wails.json ├── main.go ├── open_directory.go ├── LICENSE ├── go.mod ├── README_zh.md ├── README.md ├── go.sum ├── .github └── workflows │ └── build.yml └── app.go /.gitignore: -------------------------------------------------------------------------------- 1 | build/bin 2 | node_modules 3 | frontend/dist -------------------------------------------------------------------------------- /frontend/package.json.md5: -------------------------------------------------------------------------------- 1 | cf7eeb812cadce6dc9108a9563da0e8c -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | wailsjs/ 3 | package-lock.json -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/images/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/app.png -------------------------------------------------------------------------------- /docs/images/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/cat.png -------------------------------------------------------------------------------- /docs/images/app_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/app_zh.png -------------------------------------------------------------------------------- /docs/images/appnew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/appnew.png -------------------------------------------------------------------------------- /docs/images/cailg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/cailg.png -------------------------------------------------------------------------------- /docs/images/darkcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/darkcat.jpg -------------------------------------------------------------------------------- /docs/images/darkhw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/darkhw.png -------------------------------------------------------------------------------- /docs/images/token1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/token1.png -------------------------------------------------------------------------------- /docs/images/token2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/token2.png -------------------------------------------------------------------------------- /docs/images/token3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/token3.png -------------------------------------------------------------------------------- /docs/images/token4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/token4.png -------------------------------------------------------------------------------- /frontend/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /docs/images/appnew_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/appnew_en.png -------------------------------------------------------------------------------- /docs/images/darkandroid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/darkandroid.png -------------------------------------------------------------------------------- /docs/images/privatesetting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/docs/images/privatesetting.png -------------------------------------------------------------------------------- /frontend/commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | cd "$(dirname "$0")/../frontend" 5 | npx --no-install lint-staged 6 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | cd "$(dirname "$0")/../frontend" 5 | npx --no-install commitlint --edit "$1" 6 | -------------------------------------------------------------------------------- /frontend/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/src/assets/images/logo-universal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/frontend/src/assets/images/logo-universal.png -------------------------------------------------------------------------------- /cmd_nonwindows.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package main 4 | 5 | import "os/exec" 6 | 7 | func configureCommand(cmd *exec.Cmd, hideWindow bool) {} 8 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/nunito-v16-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmrlft/GreenWall/HEAD/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": false, 4 | "semi": true, 5 | "trailingComma": "es5", 6 | "printWidth": 100, 7 | "tabWidth": 2, 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | import eslint from 'vite-plugin-eslint'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), eslint()], 8 | }); 9 | -------------------------------------------------------------------------------- /cmd_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package main 4 | 5 | import ( 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | // configureCommand applies platform specific process settings. 11 | func configureCommand(cmd *exec.Cmd, hideWindow bool) { 12 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: hideWindow} 13 | } 14 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | green-wall 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './style.css'; 4 | import './tailwind.css'; 5 | import App from './App'; 6 | 7 | const container = document.getElementById('root'); 8 | 9 | const root = createRoot(container!); 10 | 11 | root.render( 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /wails.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://wails.io/schemas/config.v2.json", 3 | "name": "green-wall", 4 | "outputfilename": "green-wall", 5 | "frontend:install": "npm install", 6 | "frontend:build": "npm run build", 7 | "frontend:dev:watcher": "npm run dev", 8 | "frontend:dev:serverUrl": "auto", 9 | "author": { 10 | "name": "zmrlft", 11 | "email": "2643895326@qq.com" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/css/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin mobile-layout { 2 | @media screen and (max-width: 768px) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin pc-layout { 8 | @media screen and (min-width: 768px) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin tablet-layout { 14 | @media screen and (max-width: 1200px) { 15 | @content; 16 | } 17 | } 18 | 19 | /* 手机屏下的文字区域左右边距 */ 20 | $margin-mobile: 12px; 21 | 22 | /* 限制最大内容宽度,人的视角有限,文字内容太长不好阅读。*/ 23 | $max-content: 900px; 24 | -------------------------------------------------------------------------------- /docs/githubtoken.md: -------------------------------------------------------------------------------- 1 | # 如何获取你的GitHub Personal Access Token (classic) 2 | 3 | > English: [How to get your PAT](githubtoken_en.md) 4 | 5 | 打开:https://github.com/settings/tokens/new 6 | 按下图设置, Note随意命名,Expiration选择No Expiration,以免过期。给repo和user权限。 7 | ![alt text](images/token1.png) 8 | ![alt text](images/token2.png) 9 | 10 | 点击Generate token按钮生成 11 | ![alt text](images/token3.png) 12 | 复制Token到登录页面中的PAT输入框,点击登录。 13 | 注意:PAT 正是 Personal Access Token 的缩写,保管好PAT 请不要告诉任何人! 14 | ![alt text](images/token4.png) 15 | -------------------------------------------------------------------------------- /frontend/wailsjs/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wailsapp/runtime", 3 | "version": "2.0.0", 4 | "description": "Wails Javascript runtime library", 5 | "main": "runtime.js", 6 | "types": "runtime.d.ts", 7 | "scripts": { 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/wailsapp/wails.git" 12 | }, 13 | "keywords": [ 14 | "Wails", 15 | "Javascript", 16 | "Go" 17 | ], 18 | "author": "Lea Anthony ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wailsapp/wails/issues" 22 | }, 23 | "homepage": "https://github.com/wailsapp/wails#readme" 24 | } 25 | -------------------------------------------------------------------------------- /frontend/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": [ 21 | { 22 | "path": "./tsconfig.node.json" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: white; 3 | text-align: center; 4 | color: black; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | color: black; 10 | font-family: 11 | 'Nunito', 12 | -apple-system, 13 | BlinkMacSystemFont, 14 | 'Segoe UI', 15 | 'Roboto', 16 | 'Oxygen', 17 | 'Ubuntu', 18 | 'Cantarell', 19 | 'Fira Sans', 20 | 'Droid Sans', 21 | 'Helvetica Neue', 22 | sans-serif; 23 | } 24 | 25 | @font-face { 26 | font-family: 'Nunito'; 27 | font-style: normal; 28 | font-weight: 400; 29 | src: 30 | local(''), 31 | url('assets/fonts/nunito-v16-latin-regular.woff2') format('woff2'); 32 | } 33 | 34 | #app { 35 | height: 100vh; 36 | text-align: center; 37 | } 38 | -------------------------------------------------------------------------------- /docs/githubtoken_en.md: -------------------------------------------------------------------------------- 1 | # How to Generate Your GitHub Personal Access Token (classic) 2 | 3 | > 中文: [如何获取 PAT](githubtoken.md) 4 | 5 | 1. Open . 6 | 2. Configure the form as shown below: 7 | - `Note` can be any name you like. 8 | - Set `Expiration` to **No expiration** so the token never expires unexpectedly. 9 | - Select the **repo** and **user** scopes. 10 | 11 | ![token step 1](images/token1.png) 12 | ![token step 2](images/token2.png) 13 | 14 | 3. Click **Generate token**. 15 | 16 | ![token step 3](images/token3.png) 17 | 18 | 4. Copy the generated token into the PAT input field on the login screen and click **Log in**. 19 | 20 | ![token step 4](images/token4.png) 21 | 22 | > **Note:** PAT stands for *Personal Access Token*. Store it securely and never share it with anyone. 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/wailsapp/wails/v2" 7 | "github.com/wailsapp/wails/v2/pkg/options" 8 | "github.com/wailsapp/wails/v2/pkg/options/assetserver" 9 | ) 10 | 11 | //go:embed all:frontend/dist 12 | var assets embed.FS 13 | 14 | func main() { 15 | // Create an instance of the app structure 16 | app := NewApp() 17 | 18 | // Create application with options 19 | err := wails.Run(&options.App{ 20 | Title: "green-wall", 21 | // Give the contribution canvas extra breathing room so wide layouts don't get cramped. 22 | Width: 1290, 23 | Height: 750, 24 | AssetServer: &assetserver.Options{ 25 | Assets: assets, 26 | }, 27 | BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, 28 | OnStartup: app.startup, 29 | Bind: []interface{}{ 30 | app, 31 | }, 32 | }) 33 | 34 | if err != nil { 35 | println("Error:", err.Error()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/main/App.d.ts: -------------------------------------------------------------------------------- 1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 2 | // This file is automatically generated. DO NOT EDIT 3 | import {main} from '../models'; 4 | 5 | export function AuthenticateWithToken(arg1:main.GithubAuthRequest):Promise; 6 | 7 | export function CheckGitInstalled():Promise; 8 | 9 | export function ExportContributions(arg1:main.ExportContributionsRequest):Promise; 10 | 11 | export function GenerateRepo(arg1:main.GenerateRepoRequest):Promise; 12 | 13 | export function GetGithubLoginStatus():Promise; 14 | 15 | export function ImportContributions():Promise; 16 | 17 | export function LogoutGithub():Promise; 18 | 19 | export function SetGitPath(arg1:main.SetGitPathRequest):Promise; 20 | -------------------------------------------------------------------------------- /open_directory.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "path/filepath" 7 | "runtime" 8 | ) 9 | 10 | // openDirectory attempts to reveal the given directory in the default file explorer. 11 | func openDirectory(path string) error { 12 | if path == "" { 13 | return fmt.Errorf("no path provided") 14 | } 15 | 16 | absPath, err := filepath.Abs(path) 17 | if err != nil { 18 | return fmt.Errorf("resolve path: %w", err) 19 | } 20 | 21 | var cmd *exec.Cmd 22 | switch runtime.GOOS { 23 | case "windows": 24 | cmd = exec.Command("explorer", absPath) 25 | case "darwin": 26 | cmd = exec.Command("open", absPath) 27 | default: 28 | cmd = exec.Command("xdg-open", absPath) 29 | } 30 | 31 | hideWindow := runtime.GOOS != "windows" // keep Explorer visible on Windows 32 | configureCommand(cmd, hideWindow) 33 | 34 | if err := cmd.Start(); err != nil { 35 | return fmt.Errorf("launch file explorer: %w", err) 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import eslintConfigPrettier from 'eslint-config-prettier'; 3 | import pluginReact from 'eslint-plugin-react'; 4 | import reactHooksPlugin from 'eslint-plugin-react-hooks'; 5 | import { defineConfig } from 'eslint/config'; 6 | import globals from 'globals'; 7 | import tseslint from 'typescript-eslint'; 8 | 9 | export default defineConfig([ 10 | { 11 | ignores: ['wailsjs/**', 'dist/**', 'postcss.config.cjs'], 12 | }, 13 | { 14 | files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 15 | plugins: { js }, 16 | extends: ['js/recommended'], 17 | languageOptions: { globals: globals.browser }, 18 | }, 19 | tseslint.configs.recommended, 20 | { settings: { react: { version: 'detect' } } }, 21 | pluginReact.configs.flat.recommended, 22 | reactHooksPlugin.configs.flat.recommended, 23 | { 24 | rules: { 25 | semi: 2, 26 | eqeqeq: [2, 'always'], 27 | quotes: [2, 'single'], 28 | }, 29 | }, 30 | eslintConfigPrettier, 31 | ]); 32 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/main/App.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | export function AuthenticateWithToken(arg1) { 6 | return window['go']['main']['App']['AuthenticateWithToken'](arg1); 7 | } 8 | 9 | export function CheckGitInstalled() { 10 | return window['go']['main']['App']['CheckGitInstalled'](); 11 | } 12 | 13 | export function ExportContributions(arg1) { 14 | return window['go']['main']['App']['ExportContributions'](arg1); 15 | } 16 | 17 | export function GenerateRepo(arg1) { 18 | return window['go']['main']['App']['GenerateRepo'](arg1); 19 | } 20 | 21 | export function GetGithubLoginStatus() { 22 | return window['go']['main']['App']['GetGithubLoginStatus'](); 23 | } 24 | 25 | export function ImportContributions() { 26 | return window['go']['main']['App']['ImportContributions'](); 27 | } 28 | 29 | export function LogoutGithub() { 30 | return window['go']['main']['App']['LogoutGithub'](); 31 | } 32 | 33 | export function SetGitPath(arg1) { 34 | return window['go']['main']['App']['SetGitPath'](arg1); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 橡皮膏 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 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "prepare": "git config core.hooksPath .husky", 11 | "lint-staged": "lint-staged --allow-empty", 12 | "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx" 13 | }, 14 | "lint-staged": { 15 | "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", 16 | "**/*.{js,jsx,tsx,ts,less,md,json}": [ 17 | "prettier --write" 18 | ] 19 | }, 20 | "dependencies": { 21 | "clsx": "^2.1.1", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0" 24 | }, 25 | "devDependencies": { 26 | "@commitlint/cli": "^20.1.0", 27 | "@commitlint/config-conventional": "^20.0.0", 28 | "@eslint/js": "^9.38.0", 29 | "@types/react": "^18.0.17", 30 | "@types/react-dom": "^18.0.6", 31 | "@vitejs/plugin-react": "^2.0.1", 32 | "autoprefixer": "^10.4.21", 33 | "eslint": "^9.38.0", 34 | "eslint-config-prettier": "^10.1.8", 35 | "eslint-plugin-react": "^7.37.5", 36 | "eslint-plugin-react-hooks": "^7.0.1", 37 | "globals": "^16.4.0", 38 | "husky": "^9.1.7", 39 | "lint-staged": "^16.2.6", 40 | "prettier": "^3.6.2", 41 | "sass": "^1.93.2", 42 | "tailwindcss": "^3.4.18", 43 | "typescript": "^4.6.4", 44 | "typescript-eslint": "^8.46.2", 45 | "vite": "^3.0.7", 46 | "vite-plugin-eslint": "^1.8.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module green-wall 2 | 3 | go 1.24.0 4 | 5 | require github.com/wailsapp/wails/v2 v2.10.2 6 | 7 | require ( 8 | github.com/bep/debounce v1.2.1 // indirect 9 | github.com/go-ole/go-ole v1.3.0 // indirect 10 | github.com/godbus/dbus/v5 v5.1.0 // indirect 11 | github.com/google/uuid v1.6.0 // indirect 12 | github.com/gorilla/websocket v1.5.3 // indirect 13 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect 14 | github.com/labstack/echo/v4 v4.13.3 // indirect 15 | github.com/labstack/gommon v0.4.2 // indirect 16 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect 17 | github.com/leaanthony/gosod v1.0.4 // indirect 18 | github.com/leaanthony/slicer v1.6.0 // indirect 19 | github.com/leaanthony/u v1.1.1 // indirect 20 | github.com/mattn/go-colorable v0.1.13 // indirect 21 | github.com/mattn/go-isatty v0.0.20 // indirect 22 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 23 | github.com/pkg/errors v0.9.1 // indirect 24 | github.com/rivo/uniseg v0.4.7 // indirect 25 | github.com/samber/lo v1.49.1 // indirect 26 | github.com/tkrajina/go-reflector v0.5.8 // indirect 27 | github.com/valyala/bytebufferpool v1.0.0 // indirect 28 | github.com/valyala/fasttemplate v1.2.2 // indirect 29 | github.com/wailsapp/go-webview2 v1.0.19 // indirect 30 | github.com/wailsapp/mimetype v1.4.1 // indirect 31 | golang.org/x/crypto v0.45.0 // indirect 32 | golang.org/x/net v0.47.0 // indirect 33 | golang.org/x/sys v0.38.0 // indirect 34 | golang.org/x/text v0.31.0 // indirect 35 | ) 36 | 37 | // replace github.com/wailsapp/wails/v2 v2.10.2 => C:\Users\admin\go\pkg\mod 38 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # README (中文) 2 | 3 | > English: [README (English)](README.md) 4 | 5 | ## 项目里程碑: 6 | 7 | 11月初,本项目获阮一峰大佬的推荐,正式收录于[科技爱好者周刊372期](https://www.ruanyifeng.com/blog/2025/11/weekly-issue-372.html);11月中旬,获知名大V“it咖啡馆”推荐,正式收录于[Github一周热点93期](https://youtu.be/pjQftatKpjc?si=5pMK1bAyFXfp6oyF);12月,以“有趣的项目”身份顺利入选知名开源社区“你好Github”Featured|HelloGitHub 8 | 9 | ## 如何使用 10 | 11 | 请确保你的电脑已经安装了 git。 12 | 13 | ![app screenshot](/docs/images/app_zh.png) 14 | 15 | 下载软件,打开后,首先要获取你的PAT来登录github,你可以参考这个:[如何获取你的github访问令牌](docs/githubtoken.md) 16 | 17 | 登录成功左上角会显示你的头像和名字。拖动鼠标在日历上尽情画画,发挥你的艺术才能!画完后点击创建远程仓库,你可以自定义仓库名称和描述,选择仓库是否公开,确认无误后点击生成并且推送,软件会自动在你的GitHub上创建对应的仓库。 18 | 19 | 注意: GitHub 可能需要 5 分钟至两天才会显示你的贡献度图案。你可以把仓库设置为私人仓库,并在贡献统计中允许显示私人仓库的贡献,这样他人看不到仓库内容但可以看到贡献记录。 20 | ![private setting screenshot](docs/images/privatesetting.png) 21 | 22 | ### 快速提示 23 | 24 | - 绘画过程中右键可以切换画笔和橡皮擦 25 | - 可以调节画笔的强度 26 | - **复制粘贴功能**:点击"复制模式"按钮进入复制模式,在日历上拖选一块区域后按 `Ctrl+C` 复制,软件会弹出"复制成功"提示。复制后,被选中区域的图案会跟随鼠标移动作为预览,你可以左键点击或按 `Ctrl+V` 粘贴到目标位置,右键取消粘贴预览。按`Ctrl+V`可以快速恢复上次复制的图案 27 | 28 | ### Windows/Linux 29 | 30 | 下载后直接点击运行即可。软件开源,报毒正常 31 | 32 | ### macOS 33 | 34 | 由于本应用暂时未进行签名服务,首次运行时可能会遇到安全限制。按以下步骤解决: 35 | 36 | ```bash 37 | cd 你的green-wall.app存在的目录 38 | sudo xattr -cr ./green-wall.app 39 | sudo xattr -r -d com.apple.quarantine ./green-wall.app 40 | ``` 41 | 42 | **提示:** 这些指令并不需要全部执行,从上往下依次尝试,如果某条指令解决了问题就无需继续执行。 43 | 44 | **警告:** 命令执行后不会自动弹出应用界面,需要手动双击应用来启动(命令只是改变了文件属性)。 45 | 46 | ## 效果图 47 | 48 | ![text](docs/images/cailg.png) 49 | ![catfish](docs/images/cat.png) 50 | ![lovecat](docs/images/darkcat.jpg) 51 | ![helloWorld](docs/images/darkhw.png) 52 | ![androidlife](docs/images/darkandroid.png) 53 | 54 | ## 开发指南 55 | 56 | - 环境准备 57 | 58 | 安装 Go 1.23+ 59 | 60 | 安装 Node.js (v22+) 61 | 62 | 安装 git 63 | 64 | - 安装依赖工具 65 | 66 | ``` 67 | go install github.com/wailsapp/wails/v2/cmd/wails@v2.10.2 68 | ``` 69 | 70 | - 项目操作 71 | 72 | 克隆仓库并进入目录: 73 | 74 | ``` 75 | git clone https://github.com/zmrlft/GreenWall.git 76 | cd GreenWall 77 | ``` 78 | 79 | 安装前端依赖: 80 | 81 | ``` 82 | cd frontend && npm install 83 | ``` 84 | 85 | 启动开发环境 86 | 87 | ``` 88 | wails dev 89 | ``` 90 | 91 | 构建 92 | 93 | ``` 94 | wails build 95 | ``` 96 | 97 | 输出路径:build/bin/ 98 | 99 | ## 未来的功能 100 | 101 | 我们可能会增加创建自定义语言仓库的功能,例如生成一个 Java 仓库并在你的主页语言占比中统计它。 102 | 103 | ## Star History 104 | 105 | [![Star History Chart](https://api.star-history.com/svg?repos=zmrlft/GreenWall&type=date&legend=top-left)](https://www.star-history.com/#zmrlft/GreenWall&type=date&legend=top-left) 106 | 107 | ## 免责 108 | 109 | 免责声明:本项目仅用于教育、演示及研究 GitHub 贡献机制,如用于求职造假,所造成后果自负。 110 | -------------------------------------------------------------------------------- /frontend/src/components/GitInstallSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useTranslations } from '../i18n'; 3 | 4 | interface GitInstallSidebarProps { 5 | onCheckAgain: () => void; 6 | } 7 | 8 | const GitInstallSidebar: React.FC = ({ onCheckAgain }) => { 9 | const { t } = useTranslations(); 10 | const [isExpanded, setIsExpanded] = useState(false); 11 | 12 | const isMac = navigator.platform.toLowerCase().includes('mac'); 13 | const isLinux = 14 | navigator.platform.toLowerCase().includes('linux') || 15 | navigator.platform.toLowerCase().includes('x11'); 16 | 17 | const getInstructions = () => { 18 | if (isMac) return t('gitInstall.instructions.mac'); 19 | if (isLinux) return t('gitInstall.instructions.linux'); 20 | return t('gitInstall.instructions.windows'); 21 | }; 22 | 23 | const getDownloadUrl = () => { 24 | if (isMac) return 'https://git-scm.com/download/mac'; 25 | if (isLinux) return 'https://git-scm.com/download/linux'; 26 | return 'https://git-scm.com/download/win'; 27 | }; 28 | 29 | return ( 30 |
31 | {/* 展开的侧边栏 */} 32 | {isExpanded && ( 33 |
34 |
35 |

{t('gitInstall.title')}

36 | 49 |
50 | 51 |
52 |

{t('gitInstall.notInstalled')}

53 |
54 | {getInstructions()} 55 |
56 | 57 | 63 | {t('gitInstall.downloadLink')} 64 | 65 | 66 | 72 |
73 |
74 | )} 75 | 76 | {/* 提示按钮 */} 77 | 100 |
101 | ); 102 | }; 103 | 104 | export default GitInstallSidebar; 105 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /frontend/src/components/ContributionCalendar.module.scss: -------------------------------------------------------------------------------- 1 | @import '../css/mixins.scss'; 2 | .container { 3 | --cell: 14px; 4 | --gap: 4px; 5 | --tile-extra: 2px; 6 | 7 | display: grid; 8 | grid-template-columns: auto repeat(53, var(--cell)); 9 | grid-template-rows: auto repeat(7, var(--cell)) auto; 10 | gap: var(--gap); 11 | 12 | width: fit-content; 13 | font-size: 12px; 14 | padding: 0px; 15 | 16 | border-radius: 0px; 17 | margin: 0 auto; 18 | background: #ffffff; 19 | box-shadow: none; 20 | 21 | // 当窗口更大(例如最大化)时,自动放大格子与字体 22 | @media (min-width: 1280px) { 23 | --cell: 14px; 24 | --gap: 4px; 25 | --tile-extra: 2px; 26 | font-size: 14px; 27 | } 28 | 29 | @media (min-width: 1600px) { 30 | --cell: 16px; 31 | --gap: 5px; 32 | --tile-extra: 2px; 33 | font-size: 15px; 34 | } 35 | 36 | // 最大化窗口时进一步放大 37 | &.maximized { 38 | --cell: 20px; 39 | --gap: 6px; 40 | --tile-extra: 2px; 41 | font-size: 16px; 42 | } 43 | 44 | // 作为备用:单独的 maximized 类,确保样式可导出与应用 45 | .maximized { 46 | --cell: 20px; 47 | --gap: 6px; 48 | --tile-extra: 2px; 49 | font-size: 16px; 50 | } 51 | 52 | @include mobile-layout { 53 | display: none; /* 太长手机显示不下 */ 54 | } 55 | } 56 | .month { 57 | grid-row: 1/2; 58 | margin-bottom: -3px; 59 | color: #000000; 60 | font-weight: 500; 61 | } 62 | 63 | .week { 64 | grid-row: 3; 65 | grid-column: 1/2; 66 | line-height: var(--cell); 67 | margin-right: 3px; 68 | color: #000000; 69 | font-weight: 500; 70 | 71 | & + .week { 72 | grid-row: 5; 73 | } 74 | 75 | & + .week + .week { 76 | grid-row: 7; 77 | } 78 | } 79 | 80 | .tiles { 81 | grid-column: 2/55; 82 | grid-row: 2/9; 83 | 84 | display: grid; 85 | grid-auto-flow: column; 86 | grid-template-columns: subgrid; 87 | grid-template-rows: subgrid; 88 | } 89 | 90 | .tile { 91 | display: block; 92 | width: calc(var(--cell) + var(--tile-extra)); 93 | height: calc(var(--cell) + var(--tile-extra)); 94 | border-radius: calc((var(--cell) + var(--tile-extra)) / 4); 95 | 96 | outline: 1px solid rgba(27, 35, 36, 0.06); 97 | outline-offset: -1px; 98 | cursor: pointer; 99 | transition: all 0.15s ease-in-out; 100 | border: 1px solid transparent; 101 | 102 | &[data-level='0'] { 103 | background: #ebedf0; 104 | border-color: rgba(235, 237, 240, 0.3); 105 | } 106 | &[data-level='1'] { 107 | background: #9be9a8; 108 | border-color: rgba(155, 233, 168, 0.3); 109 | } 110 | &[data-level='2'] { 111 | background: #40c463; 112 | border-color: rgba(64, 196, 99, 0.3); 113 | } 114 | &[data-level='3'] { 115 | background: #30a14e; 116 | border-color: rgba(48, 161, 78, 0.3); 117 | } 118 | &[data-level='4'] { 119 | background: #216e39; 120 | border-color: rgba(33, 110, 57, 0.3); 121 | } 122 | 123 | &[data-future='true'] { 124 | background: #000000 !important; 125 | border-color: #000000 !important; 126 | cursor: not-allowed; 127 | outline: none; 128 | outline-offset: 0; 129 | transform: none; 130 | box-shadow: none; 131 | 132 | &:hover { 133 | outline: none; 134 | transform: none; 135 | box-shadow: none; 136 | } 137 | } 138 | 139 | // Enhanced hover effects 140 | &:hover { 141 | outline: 2px solid #000000; 142 | outline-offset: 1px; 143 | transform: scale(1.2); 144 | box-shadow: none; 145 | z-index: 10; 146 | } 147 | 148 | // 预览状态样式 149 | &.preview { 150 | background: #216e39 !important; 151 | border-color: rgba(33, 110, 57, 0.8) !important; 152 | outline: 2px solid #ff6b35 !important; 153 | outline-offset: 1px; 154 | animation: pulse 1.5s infinite; 155 | } 156 | 157 | // 选择状态样式 - 只加边框,保持原有颜色 158 | &.selection { 159 | outline: 2px solid #2563eb !important; 160 | outline-offset: 1px; 161 | } 162 | 163 | @keyframes pulse { 164 | 0% { 165 | opacity: 1; 166 | } 167 | 50% { 168 | opacity: 0.7; 169 | } 170 | 100% { 171 | opacity: 1; 172 | } 173 | } 174 | } 175 | 176 | .total { 177 | grid-column: 2/30; 178 | margin-top: 4px; 179 | color: #000000 !important; 180 | font-size: 16px; 181 | font-weight: bold; 182 | } 183 | 184 | .legend { 185 | grid-column: 30/53; 186 | margin-top: 4px; 187 | 188 | display: flex; 189 | gap: 6px; 190 | justify-content: right; 191 | align-items: center; 192 | font-size: 11px; 193 | font-weight: 500; 194 | text-transform: uppercase; 195 | letter-spacing: 0.5px; 196 | color: #000000 !important; 197 | 198 | .tile { 199 | cursor: default; 200 | transition: none; 201 | 202 | // Disable hover effects for legend tiles 203 | &:hover { 204 | outline: none; 205 | transform: none; 206 | box-shadow: none; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README (English) 2 | 3 | > 中文: [README (中文)](README_zh.md) 4 | 5 | ## Project Milestones 6 | 7 | In early November, this project was recommended by YiFeng Ruan(阮一峰), and officially featured in [Tech Enthusiast Weekly Issue 372](https://www.ruanyifeng.com/blog/2025/11/weekly-issue-372.html); in mid-November, it was recommended by the well-known influencer "it咖啡馆" and featured in [GitHub Weekly Hotspots Issue 93](https://youtu.be/pjQftatKpjc?si=5pMK1bAyFXfp6oyF); in December, it was successfully selected as an "interesting project" by the renowned open-source community HelloGitHub Featured|HelloGitHub 8 | 9 | ## How to use 10 | 11 | Make sure Git is installed on your computer. 12 | 13 | ![app screenshot](/docs/images/app.png) 14 | 15 | Download the app, open it, and first grab a Personal Access Token (PAT) so you can sign in to GitHub. You can follow this guide: [how to get your PAT](docs/githubtoken_en.md). 16 | 17 | Once you’re logged in you’ll see your avatar and name in the upper-left corner. Drag across the calendar to paint your design. When you’re satisfied, click **Create Remote Repo**. You can edit the repo name and description, choose whether it’s public or private, and then press **Generate & Push** to let the app create and push the repository for you automatically. 18 | 19 | > **Heads-up:** GitHub may take anywhere from 5 minutes to 2 days to show the contributions on your profile. You can keep the repo private and enable “Include private contributions” in your profile settings so others can’t see the repo content but the contribution streak still counts. 20 | 21 | ![private setting screenshot](docs/images/privatesetting.png) 22 | 23 | ### Quick Tips 24 | 25 | - Right-click while painting to toggle between the brush and the eraser. 26 | - Use the brush intensity control to switch between different shades of green. 27 | - **Copy and Paste Feature**: Click the "Copy Mode" button to enter copy mode. Drag to select an area on the calendar and press `Ctrl+C` to copy. The app will show a "Copy successful" message. After copying, the selected pattern will follow the mouse as a preview. Left-click or press `Ctrl+V` to paste to the target location, right-click to cancel the paste preview. Press `Ctrl+V` to quickly restore the last copied pattern. 28 | 29 | ### Windows/Linux 30 | 31 | Download and run the application directly. 32 | 33 | ### macOS 34 | 35 | Since this application is not yet signed, you may encounter security restrictions on first launch. Follow these steps to resolve: 36 | 37 | ```bash 38 | cd the-directory-where-green-wall.app-is-located 39 | sudo xattr -cr ./green-wall.app 40 | sudo xattr -r -d com.apple.quarantine ./green-wall.app 41 | ``` 42 | 43 | **Tip:** You don't need to execute all of these commands. Try them in order from top to bottom, and stop once one resolves the issue. 44 | 45 | **Warning:** The commands will not automatically launch the application. You need to manually double-click the app to start it (the commands only modify file attributes). 46 | 47 | ## Rendering 48 | 49 | ![text](docs/images/cailg.png) 50 | ![catfish](docs/images/cat.png) 51 | ![lovecat](docs/images/darkcat.jpg) 52 | ![helloWorld](docs/images/darkhw.png) 53 | ![androidlife](docs/images/darkandroid.png) 54 | 55 | ## Development Guide 56 | 57 | - Environmental Preparation 58 | 59 | Install Go 1.23+ 60 | 61 | Install Node.js (v22+) 62 | 63 | Install Git 64 | 65 | - Install dependent tools 66 | 67 | ``` 68 | go install github.com/wailsapp/wails/v2/cmd/wails@v2.10.2 69 | ``` 70 | 71 | - Project operation 72 | 73 | Clone the repository and enter the directory: 74 | 75 | ``` 76 | git clone https://github.com/zmrlft/GreenWall.git 77 | cd GreenWall 78 | ``` 79 | 80 | Install front-end dependencies: 81 | 82 | ``` 83 | cd frontend && npm install 84 | ``` 85 | 86 | Start the development environment 87 | 88 | ``` 89 | wails dev 90 | ``` 91 | 92 | Construction 93 | 94 | ``` 95 | wails build 96 | ``` 97 | 98 | Output path: build/bin/ 99 | 100 | ## Future features 101 | 102 | We may add support for creating repositories in custom languages. For example, if you want a Java repository, the tool would generate one and it would be reflected in your GitHub language statistics. 103 | 104 | ## Star History 105 | 106 | [![Star History Chart](https://api.star-history.com/svg?repos=zmrlft/GreenWall&type=date&legend=top-left)](https://www.star-history.com/#zmrlft/GreenWall&type=date&legend=top-left) 107 | 108 | ## Disclaimer 109 | 110 | This project is provided for educational, demonstration, and research purposes related to GitHub contribution mechanics. Misuse (for example to falsify job applications) is the user's responsibility. 111 | -------------------------------------------------------------------------------- /frontend/src/components/RemoteRepoModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslations } from '../i18n'; 3 | 4 | export type RemoteRepoPayload = { 5 | name: string; 6 | description: string; 7 | isPrivate: boolean; 8 | }; 9 | 10 | type RemoteRepoModalProps = { 11 | open: boolean; 12 | defaultName: string; 13 | defaultDescription?: string; 14 | defaultPrivate?: boolean; 15 | isSubmitting?: boolean; 16 | onSubmit: (payload: RemoteRepoPayload) => void; 17 | onClose: () => void; 18 | }; 19 | 20 | const repoNamePattern = /^[a-zA-Z0-9._-]{1,100}$/; 21 | 22 | const RemoteRepoModal: React.FC = ({ 23 | open, 24 | defaultName, 25 | defaultDescription = '', 26 | defaultPrivate = true, 27 | isSubmitting = false, 28 | onSubmit, 29 | onClose, 30 | }) => { 31 | const { dictionary } = useTranslations(); 32 | const labels = dictionary.remoteModal; 33 | 34 | const [name, setName] = React.useState(defaultName); 35 | const [description, setDescription] = React.useState(defaultDescription); 36 | const [isPrivate, setIsPrivate] = React.useState(defaultPrivate); 37 | const [error, setError] = React.useState(null); 38 | 39 | React.useEffect(() => { 40 | if (open) { 41 | setName(defaultName); 42 | setDescription(defaultDescription); 43 | setIsPrivate(defaultPrivate); 44 | setError(null); 45 | } 46 | }, [open, defaultName, defaultDescription, defaultPrivate]); 47 | 48 | if (!open) { 49 | return null; 50 | } 51 | 52 | const handleSubmit = (event: React.FormEvent) => { 53 | event.preventDefault(); 54 | const trimmedName = name.trim(); 55 | if (!trimmedName) { 56 | setError(labels.nameRequired); 57 | return; 58 | } 59 | if (!repoNamePattern.test(trimmedName)) { 60 | setError(labels.nameInvalid); 61 | return; 62 | } 63 | setError(null); 64 | onSubmit({ 65 | name: trimmedName, 66 | description: description.trim(), 67 | isPrivate, 68 | }); 69 | }; 70 | 71 | return ( 72 |
73 |
74 |
75 |
76 |

{labels.title}

77 |

{labels.description}

78 |
79 | 87 |
88 | 89 |
90 | 103 | 104 |
105 | {labels.privacyLabel} 106 |
107 | 116 | 125 |
126 |
127 | 128 |