├── README.md
├── public
├── icon.icns
├── fonts
│ ├── Montserrat-Bold.ttf
│ ├── Montserrat-Medium.ttf
│ ├── JetBrainsMono-Medium.ttf
│ ├── Montserrat-BoldItalic.ttf
│ └── Montserrat-MediumItalic.ttf
├── svg_icons
│ ├── habits.svg
│ ├── plus.svg
│ ├── settings.svg
│ ├── trash.svg
│ ├── hashtag.svg
│ ├── icon.svg
│ ├── codes.svg
│ └── notes.svg
└── splash.html
├── src
├── main.jsx
├── utils
│ ├── dates.js
│ ├── readDataFromFile.js
│ └── handlers.js
├── context
│ ├── GithubProvider.jsx
│ ├── context.js
│ ├── FunctionsProvider.jsx
│ └── FoldersProvider.jsx
├── components
│ ├── Topbar.jsx
│ ├── Bottombar.jsx
│ ├── ShowCase.jsx
│ ├── SettingsMenu.jsx
│ ├── MarkdownEditor.jsx
│ └── GitHubActivity.jsx
├── App.jsx
└── index.css
├── vite.config.js
├── .gitignore
├── index.html
├── eslint.config.js
├── package.json
└── electron
└── main.js
/README.md:
--------------------------------------------------------------------------------
1 | # Hashnote
2 |
3 | 
4 |
--------------------------------------------------------------------------------
/public/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerbola/hashnote-electron/HEAD/public/icon.icns
--------------------------------------------------------------------------------
/public/fonts/Montserrat-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerbola/hashnote-electron/HEAD/public/fonts/Montserrat-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/Montserrat-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerbola/hashnote-electron/HEAD/public/fonts/Montserrat-Medium.ttf
--------------------------------------------------------------------------------
/public/fonts/JetBrainsMono-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerbola/hashnote-electron/HEAD/public/fonts/JetBrainsMono-Medium.ttf
--------------------------------------------------------------------------------
/public/fonts/Montserrat-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerbola/hashnote-electron/HEAD/public/fonts/Montserrat-BoldItalic.ttf
--------------------------------------------------------------------------------
/public/fonts/Montserrat-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerbola/hashnote-electron/HEAD/public/fonts/Montserrat-MediumItalic.ttf
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.jsx'
5 |
6 | createRoot(document.getElementById('root')).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | export default defineConfig({
4 | plugins: [react()],
5 | server: {
6 | port: 3005,
7 | },
8 | base: "./",
9 | build: {
10 | outDir: "dist",
11 | emptyOutDir: true,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/public/svg_icons/habits.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/svg_icons/plus.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/svg_icons/settings.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | release
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/public/svg_icons/trash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/svg_icons/hashtag.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/svg_icons/icon.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hashnote
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/utils/dates.js:
--------------------------------------------------------------------------------
1 | export const months = [
2 | "January",
3 | "February",
4 | "March",
5 | "April",
6 | "May",
7 | "June",
8 | "July",
9 | "August",
10 | "September",
11 | "October",
12 | "November",
13 | "December",
14 | ];
15 |
16 | const weekdays = [
17 | "Sun",
18 | "Mon",
19 | "Tue",
20 | "Wed",
21 | "Thu",
22 | "Fri",
23 | "Sat",
24 | ];
25 |
26 | export const date = [
27 | new Date().getDate(), // day
28 | months[new Date().getMonth()], // month
29 | weekdays[new Date().getDay()], // week day
30 | ];
31 |
--------------------------------------------------------------------------------
/src/context/GithubProvider.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 | import { GithubContext } from "./context";
3 | import { readToken, readUsername } from "../utils/readDataFromFile";
4 |
5 | export const GithubProvider = ({ children }) => {
6 | const [username, setUsername] = useState(readUsername());
7 | const [token, setToken] = useState(readToken());
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | export const useGithub = () => {
16 | return useContext(GithubContext);
17 | };
18 |
--------------------------------------------------------------------------------
/src/context/context.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | const initialFunctions = {
4 | editorValue: "",
5 | setEditorValue: null,
6 | filePath: "",
7 | setFilePath: null,
8 | };
9 |
10 | const initialFolders = {
11 | folders: {},
12 | setFolders: null,
13 | loadFilesFromDisk: null,
14 | };
15 |
16 | const initialGithub = {
17 | username: "",
18 | token: "",
19 | setUsername: null,
20 | setToken: null,
21 | };
22 |
23 | export const FunctionsContext = createContext(initialFunctions);
24 | export const FoldersContext = createContext(initialFolders);
25 | export const GithubContext = createContext(initialGithub);
26 |
--------------------------------------------------------------------------------
/src/components/Topbar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { date } from "../utils/dates";
3 | import SettingsMenu from "./SettingsMenu";
4 | const Topbar = () => {
5 | const [active, setActive] = useState(false);
6 | const addZero = (number) => {
7 | return number < 10 ? "0" + number : number;
8 | };
9 | return (
10 |
11 |
12 |
{addZero(date[0])}
13 |
14 |
{date[2]}
15 |
{date[1]}
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default Topbar;
29 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 |
6 | export default [
7 | { ignores: ['dist'] },
8 | {
9 | files: ['**/*.{js,jsx}'],
10 | languageOptions: {
11 | ecmaVersion: 2020,
12 | globals: globals.browser,
13 | parserOptions: {
14 | ecmaVersion: 'latest',
15 | ecmaFeatures: { jsx: true },
16 | sourceType: 'module',
17 | },
18 | },
19 | plugins: {
20 | 'react-hooks': reactHooks,
21 | 'react-refresh': reactRefresh,
22 | },
23 | rules: {
24 | ...js.configs.recommended.rules,
25 | ...reactHooks.configs.recommended.rules,
26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27 | 'react-refresh/only-export-components': [
28 | 'warn',
29 | { allowConstantExport: true },
30 | ],
31 | },
32 | },
33 | ]
34 |
--------------------------------------------------------------------------------
/public/splash.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Loading...
7 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/public/svg_icons/codes.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/context/FunctionsProvider.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from "react";
2 | import { FunctionsContext } from "./context";
3 | import { readFile } from "../utils/readDataFromFile";
4 |
5 | export const FunctionsProvider = ({ children }) => {
6 | const [editorValue, setEditorValue] = useState("");
7 | const [filePath, setFilePath] = useState("");
8 |
9 | useEffect(() => {
10 | if (!filePath) return;
11 |
12 | const readValue = async () => {
13 | try {
14 | const data = await readFile(filePath);
15 | setEditorValue(data);
16 | } catch (error) {
17 | console.error("Failed to read file:", error);
18 | }
19 | };
20 |
21 | readValue();
22 | }, [filePath]);
23 |
24 | return (
25 |
28 | {children}
29 |
30 | );
31 | };
32 |
33 | export const useFunctions = () => {
34 | return useContext(FunctionsContext);
35 | };
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hashnote",
3 | "private": true,
4 | "type": "module",
5 | "version": "1.0.0-beta",
6 | "description": "A simple markdown and note-taking app",
7 | "author": "Mutawirr",
8 | "main": "electron/main.js",
9 | "build": {
10 | "appId": "com.hashnote.mutawirr",
11 | "productName": "Hashnote",
12 | "icon": "public/icon.icns",
13 | "mac": {
14 | "target": "dmg"
15 | },
16 | "files": [
17 | "dist/",
18 | "electron/**/*",
19 | "public/**/**"
20 | ],
21 | "directories": {
22 | "buildResources": "public",
23 | "output": "release"
24 | }
25 | },
26 | "mac": {
27 | "identity": null
28 | },
29 | "scripts": {
30 | "dev": "vite",
31 | "build": "vite build",
32 | "lint": "eslint .",
33 | "start": "npx electron .",
34 | "build-renderer": "vite build",
35 | "build-electron": "electron-builder",
36 | "package": "npm run build-renderer && npm run build-electron"
37 | },
38 | "devDependencies": {
39 | "@eslint/js": "^9.21.0",
40 | "@mdxeditor/editor": "^3.29.1",
41 | "@types/react": "^19.0.10",
42 | "@types/react-dom": "^19.0.4",
43 | "@vitejs/plugin-react": "^4.3.4",
44 | "electron": "^35.1.2",
45 | "electron-builder": "^26.0.12",
46 | "eslint": "^9.21.0",
47 | "eslint-plugin-react-hooks": "^5.1.0",
48 | "eslint-plugin-react-refresh": "^0.4.19",
49 | "globals": "^15.15.0",
50 | "marked": "^15.0.7",
51 | "react": "^19.0.0",
52 | "react-dom": "^19.0.0",
53 | "vite": "^6.2.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/Bottombar.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import GitHubActivity from "./GitHubActivity";
3 | import ShowCase from "./ShowCase";
4 | import { readToken } from "../utils/readDataFromFile";
5 | import { useFolders } from "../context/FoldersProvider";
6 |
7 | const Bottombar = ({ setActiveFile }) => {
8 | const [activeFolder, setActiveFolder] = useState("Notes");
9 | const { folders, loadFilesFromDisk } = useFolders();
10 | useEffect(() => {
11 | loadFilesFromDisk();
12 | readToken();
13 | }, []);
14 |
15 | useEffect(() => {
16 | loadFilesFromDisk();
17 | }, [activeFolder]);
18 |
19 | return (
20 |
21 |
22 | {/* Show active folder data */}
23 |
24 |
25 |
26 |
27 | {Object.keys(folders).map((folderKey) => (
28 |
45 | ))}
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default Bottombar;
53 |
--------------------------------------------------------------------------------
/public/svg_icons/notes.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/electron/main.js:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow } from "electron";
2 | import path from "node:path";
3 | import { fileURLToPath } from "node:url";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = path.dirname(__filename);
7 |
8 | let mainWindow;
9 | let splashWindow;
10 |
11 | function createWindow() {
12 | // Splash screen window
13 | splashWindow = new BrowserWindow({
14 | width: 600,
15 | height: 600,
16 | frame: false,
17 | alwaysOnTop: true,
18 | transparent: true,
19 | resizable: false,
20 | });
21 |
22 | splashWindow.loadURL(`file://${path.join(__dirname, "../public/splash.html")}`);
23 |
24 | // Main window
25 | mainWindow = new BrowserWindow({
26 | width: 600,
27 | height: 600,
28 | icon: path.join(__dirname, "../public/icon.icns"),
29 | frame: false,
30 | resizable: false,
31 | transparent: true,
32 | backgroundColor: "#151515",
33 | webPreferences: {
34 | nodeIntegration: true,
35 | contextIsolation: false,
36 | },
37 | });
38 |
39 | const startURL = app.isPackaged
40 | ? `file://${path.join(__dirname, "../dist/index.html")}`
41 | : "http://localhost:3005";
42 |
43 | mainWindow.loadURL(startURL);
44 |
45 | mainWindow.once("ready-to-show", () => {
46 | // Close splash and show main window
47 | if (splashWindow) {
48 | splashWindow.close();
49 | }
50 | mainWindow.show();
51 | });
52 | }
53 |
54 | app.whenReady().then(() => {
55 | createWindow();
56 |
57 | if (process.platform === "darwin") {
58 | app.dock.setIcon(path.join(__dirname, "public", "icon.icns"));
59 | }
60 |
61 | app.on("activate", () => {
62 | if (BrowserWindow.getAllWindows().length === 0) createWindow();
63 | });
64 | });
65 |
66 | app.on("window-all-closed", () => {
67 | if (process.platform !== "darwin") app.quit();
68 | });
69 |
--------------------------------------------------------------------------------
/src/utils/readDataFromFile.js:
--------------------------------------------------------------------------------
1 | const fs = window.require("fs");
2 | const path = window.require("path");
3 | const os = window.require("os");
4 | const homeDir = os.homedir();
5 | const appBaseDir = path.join(homeDir, ".hashnote");
6 |
7 | export const readToken = () => {
8 | try {
9 | // Get directory
10 | const filePath = path.join(homeDir, ".hashnote", "token.txt");
11 |
12 | if (!fs.existsSync(appBaseDir)) {
13 | fs.mkdirSync(appBaseDir, { recursive: true });
14 | }
15 |
16 | // Check if token file exists
17 | if (fs.existsSync(filePath)) {
18 | const token = fs.readFileSync(filePath, "utf-8");
19 | return token;
20 | } else {
21 | console.warn("Token file does not exist. So created one.");
22 | fs.writeFileSync(filePath, "");
23 | return "";
24 | }
25 | } catch (e) {
26 | console.error("Error reading token:", e);
27 | return "";
28 | }
29 | };
30 |
31 | export const readUsername = () => {
32 | try {
33 | // Get directory
34 | const filePath = path.join(homeDir, ".hashnote", "username.txt");
35 | // Check if username file exists
36 | if (fs.existsSync(filePath)) {
37 | const username = fs.readFileSync(filePath, "utf-8");
38 | return username;
39 | } else {
40 | console.warn("Username file does not exist. So created one.");
41 | fs.writeFileSync(filePath, "");
42 | return "";
43 | }
44 | } catch (e) {
45 | console.error("Error reading username:", e);
46 | return "";
47 | }
48 | };
49 |
50 | export const readFile = async (filePath) => {
51 | return new Promise((resolve, reject) => {
52 | try {
53 | if (fs.existsSync(filePath)) {
54 | const fileValue = fs.readFileSync(filePath, "utf-8");
55 | resolve(fileValue);
56 | } else {
57 | console.warn("File does not exist.");
58 | resolve("");
59 | }
60 | } catch (e) {
61 | console.error("Error reading file:", e);
62 | reject(e);
63 | }
64 | });
65 | };
66 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import Topbar from "./components/Topbar";
3 | import Bottombar from "./components/Bottombar";
4 | const MarkdownEditor = React.lazy(() => import("./components/MarkdownEditor"));
5 | import { FunctionsProvider } from "./context/FunctionsProvider";
6 | import { FoldersProvider } from "./context/FoldersProvider";
7 | import { GithubProvider } from "./context/GithubProvider";
8 |
9 | function App() {
10 | const [activeFile, setActiveFile] = useState(false);
11 | const bottomRef = useRef(null);
12 | const editorRef = useRef(null);
13 |
14 | useEffect(() => {
15 | if (!bottomRef.current || !editorRef.current) return;
16 |
17 | if (activeFile) {
18 | bottomRef.current.style.opacity = 0;
19 | editorRef.current.style.display = "block";
20 |
21 | setTimeout(() => {
22 | bottomRef.current.style.display = "none";
23 | editorRef.current.style.opacity = 1;
24 | }, 200);
25 | } else {
26 | editorRef.current.style.opacity = 0;
27 | setTimeout(() => {
28 | editorRef.current.style.display = "none";
29 | bottomRef.current.style.display = "block";
30 | setTimeout(() => {
31 | bottomRef.current.style.opacity = 1;
32 | }, 10);
33 | }, 190);
34 | }
35 | }, [activeFile]);
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
52 |
53 |
54 |
55 |
56 | );
57 | }
58 |
59 | export default App;
60 |
--------------------------------------------------------------------------------
/src/context/FoldersProvider.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 | import { FoldersContext } from "./context";
3 |
4 | export const FoldersProvider = ({ children }) => {
5 | const [folders, setFolders] = useState({
6 | Notes: {
7 | title: "Notes",
8 | icon: "./svg_icons/notes.svg",
9 | data: [],
10 | },
11 | Habits: {
12 | title: "Habits",
13 | icon: "./svg_icons/habits.svg",
14 | data: [],
15 | },
16 | Codes: {
17 | title: "Codes",
18 | icon: "./svg_icons/codes.svg",
19 | data: [],
20 | },
21 | });
22 |
23 | const loadFilesFromDisk = () => {
24 | setFolders((prev) => {
25 | const fs = window.require("fs");
26 | const path = window.require("path");
27 | const os = window.require("os");
28 |
29 | const homeDir = os.homedir();
30 | const appBaseDir = path.join(homeDir, ".hashnote");
31 | const updated = { ...prev };
32 |
33 | Object.keys(updated).forEach((folderKey) => {
34 | const directory = path.join(appBaseDir, folderKey.toLowerCase());
35 |
36 | if (!fs.existsSync(directory)) {
37 | fs.mkdirSync(directory, { recursive: true });
38 | }
39 |
40 | const files = fs.readdirSync(directory);
41 |
42 | const filesWithStats = files.map((fileName) => {
43 | const fullPath = path.join(directory, fileName);
44 | const stats = fs.statSync(fullPath); // get file metadata
45 |
46 | return {
47 | title: fileName.split(".")[0].split("-").join(" "),
48 | path: fullPath,
49 | createdAt: stats.birthtime, // or stats.ctime
50 | };
51 | });
52 |
53 | // Sort newest first
54 | filesWithStats.sort((a, b) => b.createdAt - a.createdAt);
55 |
56 | updated[folderKey].data = filesWithStats;
57 | });
58 |
59 | return updated;
60 | });
61 | };
62 |
63 | return (
64 |
65 | {children}
66 |
67 | );
68 | };
69 |
70 | export const useFolders = () => {
71 | return useContext(FoldersContext);
72 | };
73 |
--------------------------------------------------------------------------------
/src/components/ShowCase.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { handleCreateNewFile, handleDeleteFile } from "../utils/handlers";
3 | import { useFolders } from "../context/FoldersProvider";
4 | import { useFunctions } from "../context/FunctionsProvider";
5 |
6 | const ShowCase = ({ data, setActiveFile }) => {
7 | const { loadFilesFromDisk } = useFolders();
8 | const [datas, setDatas] = useState([]);
9 | const [opacity, setOpacity] = useState(1);
10 | const { setFilePath } = useFunctions();
11 | useEffect(() => {
12 | setOpacity(0);
13 | const timeout = setTimeout(() => {
14 | setDatas(data);
15 | setOpacity(1);
16 | }, 150);
17 | return () => clearTimeout(timeout);
18 | }, [data]);
19 |
20 | return (
21 |
27 |
28 |
29 |
{datas.title}
30 | {datas.title &&

}
31 |
32 |
33 |
39 |
40 |
41 |
42 | {datas.data?.map((note, idx) => (
43 |
{
47 | setActiveFile(true);
48 | setFilePath("");
49 | setTimeout(() => {
50 | setFilePath(note.path);
51 | }, 0);
52 | }}
53 | >
54 |

55 |
56 |
{note.title}
57 |

{
63 | e.stopPropagation();
64 | handleDeleteFile(note, idx, e, datas, loadFilesFromDisk);
65 | }}
66 | />
67 |
68 |
69 | ))}
70 |
71 |
72 | );
73 | };
74 |
75 | export default ShowCase;
76 |
--------------------------------------------------------------------------------
/src/components/SettingsMenu.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { handleSaveToFile } from "../utils/handlers";
3 | import { useGithub } from "../context/GithubProvider";
4 | const shell = window.require("electron").shell;
5 |
6 | const SettingsMenu = ({ active, setActive }) => {
7 | const { username, setUsername, token, setToken } = useGithub();
8 | const [fieldValues, setFieldValues] = useState({ username, token });
9 | const handleOnChange = (name, value) => {
10 | setFieldValues((prev) => ({ ...prev, [name]: value }));
11 | };
12 |
13 | const path = window.require("path");
14 | const os = window.require("os");
15 |
16 | const homeDir = os.homedir(); // ~/
17 | const appBaseDir = path.join(homeDir, ".hashnote"); // ~/.hashnote
18 |
19 | const handleClose = () => {
20 | setActive(!active);
21 | // Username saving
22 | const usernamePath = path.join(appBaseDir, "username.txt");
23 | setUsername(fieldValues.username);
24 | handleSaveToFile(usernamePath, fieldValues.username);
25 | // Token saving
26 | const tokenPath = path.join(appBaseDir, "token.txt");
27 | setToken(fieldValues.token);
28 | handleSaveToFile(tokenPath, fieldValues.token);
29 | };
30 |
31 | return (
32 |
36 |
37 |
Settings
38 |
41 |
42 |
43 |
44 |
Github Username
45 | handleOnChange("username", e.target.value)}
49 | value={fieldValues.username}
50 | />
51 |
52 |
53 |
Github Token
54 |
60 |
61 |
76 |
77 | );
78 | };
79 |
80 | export default SettingsMenu;
81 |
--------------------------------------------------------------------------------
/src/utils/handlers.js:
--------------------------------------------------------------------------------
1 | const fs = window.require("fs");
2 | const path = window.require("path");
3 | const os = window.require("os");
4 |
5 | const homeDir = os.homedir(); // ~/
6 | const appBaseDir = path.join(homeDir, ".hashnote"); // ~/.hashnote
7 |
8 | export const handleSaveToFile = (filePath, content) => {
9 | try {
10 | if (!filePath) {
11 | console.error("Error: filePath is empty or undefined.");
12 | return;
13 | }
14 |
15 | if (!content) {
16 | console.warn("Error: content is empty.");
17 | content = "";
18 | }
19 |
20 | if (!fs.existsSync(appBaseDir)) {
21 | fs.mkdirSync(appBaseDir, { recursive: true });
22 | }
23 | fs.writeFileSync(filePath, content, "utf-8");
24 | } catch (e) {
25 | console.error(`Error saving ${filePath}:`, e);
26 | }
27 | };
28 |
29 | export const handleCreateNewFile = (datas, loadFilesFromDisk) => {
30 | try {
31 | const folderPath = path.join(appBaseDir, datas.title.toLowerCase());
32 |
33 | if (!fs.existsSync(folderPath)) {
34 | fs.mkdirSync(folderPath, { recursive: true });
35 | }
36 | const name = datas?.title?.split("s")[0];
37 | const filePath = path.join(folderPath, `New-${name}.md`);
38 |
39 | if (!fs.existsSync(filePath)) {
40 | fs.writeFileSync(filePath, `# New ${name}`);
41 | } else {
42 | console.warn("File already exist");
43 | return "";
44 | }
45 |
46 | loadFilesFromDisk();
47 | return filePath;
48 | } catch (error) {
49 | console.error("Error saving file:", error);
50 | }
51 | };
52 |
53 | export const handleDeleteFile = (note, index, e, datas, loadFilesFromDisk) => {
54 | e.stopPropagation();
55 | try {
56 | let filePath = note.path;
57 | if (!filePath) {
58 | const folderPath = path.join(appBaseDir, datas.title.toLowerCase());
59 | filePath = path.join(folderPath, note.title);
60 | }
61 |
62 | if (!fs.existsSync(filePath)) {
63 | console.warn(`File not found for deletion: ${filePath}`);
64 |
65 | setDatas((prev) => {
66 | const updatedData = { ...prev };
67 | updatedData.data = updatedData.data.filter((_, i) => i !== index);
68 | return updatedData;
69 | });
70 | return;
71 | }
72 |
73 | fs.unlinkSync(filePath);
74 | loadFilesFromDisk();
75 | } catch (error) {
76 | console.error("Error deleting file:", error);
77 | }
78 | };
79 |
80 | export const handleRenameFile = (filePath, newFileName, loadFilesFromDisk) => {
81 | return new Promise((resolve, reject) => {
82 | const fs = window.require("fs");
83 | const path = window.require("path");
84 |
85 | if (!fs.existsSync(filePath)) {
86 | return reject(new Error("File does not exist"));
87 | }
88 |
89 | const targetDir = path.dirname(filePath);
90 | const newFilePath = path.join(targetDir, newFileName);
91 |
92 | try {
93 | fs.renameSync(filePath, newFilePath);
94 | loadFilesFromDisk();
95 | resolve(newFilePath);
96 | } catch (err) {
97 | console.error("Error during file rename:", err);
98 | reject(err);
99 | }
100 | });
101 | };
102 |
--------------------------------------------------------------------------------
/src/components/MarkdownEditor.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | MDXEditor,
3 | headingsPlugin,
4 | linkPlugin,
5 | listsPlugin,
6 | markdownShortcutPlugin,
7 | quotePlugin,
8 | thematicBreakPlugin,
9 | } from "@mdxeditor/editor";
10 | import "@mdxeditor/editor/style.css";
11 | import { useEffect, useRef } from "react";
12 | import { useFunctions } from "../context/FunctionsProvider";
13 | import { useFolders } from "../context/FoldersProvider";
14 | import { handleRenameFile, handleSaveToFile } from "../utils/handlers";
15 |
16 | const MarkdownEditor = ({ editorRef, setActiveFile }) => {
17 | const { editorValue, setEditorValue, filePath } = useFunctions();
18 | const { loadFilesFromDisk } = useFolders();
19 | const mdxEditorRef = useRef(null);
20 | const filePathRef = useRef(filePath);
21 | const editorValueRef = useRef(editorValue);
22 | const lastTriggerTimeRef = useRef(0);
23 |
24 | useEffect(() => {
25 | filePathRef.current = filePath;
26 | }, [filePath]);
27 |
28 | useEffect(() => {
29 | editorValueRef.current = editorValue;
30 | if (mdxEditorRef.current && mdxEditorRef.current.setMarkdown) {
31 | mdxEditorRef.current.setMarkdown(editorValue);
32 | }
33 | }, [editorValue]);
34 |
35 | const exitAndSave = async (e) => {
36 | const now = Date.now();
37 |
38 | if (e.deltaX < -20 && now - lastTriggerTimeRef.current > 500) {
39 | setActiveFile(false);
40 | lastTriggerTimeRef.current = now;
41 | } else {
42 | return;
43 | }
44 |
45 | if (!filePathRef.current || filePathRef.current.trim() === "") {
46 | console.error("Error: filePath is empty or undefined.");
47 | return;
48 | }
49 |
50 | const content = editorValueRef.current;
51 | const titleMatch = content.match(/^# (.+)/m);
52 |
53 | if (!titleMatch) {
54 | console.error("Error: No title found in the file.");
55 | return;
56 | }
57 |
58 | const title = titleMatch[1].trim();
59 | const currentFileName = filePathRef.current.split("/").pop().split(".")[0];
60 | const newFileName = title.replace(/\s+/g, "-") + ".md";
61 |
62 | if (title !== currentFileName) {
63 | try {
64 | const newFilePath = await handleRenameFile(
65 | filePathRef.current,
66 | newFileName,
67 | loadFilesFromDisk
68 | );
69 | filePathRef.current = newFilePath;
70 | } catch (err) {
71 | console.error("Rename failed:", err);
72 | return;
73 | }
74 | }
75 |
76 | // Now save to the new file path
77 | handleSaveToFile(filePathRef.current, content);
78 | };
79 |
80 | useEffect(() => {
81 | const wrapper = document.getElementById("mdx-wrapper");
82 | if (!wrapper) return;
83 | wrapper.addEventListener("wheel", exitAndSave);
84 | return () => wrapper.removeEventListener("wheel", exitAndSave);
85 | }, []);
86 |
87 | const handleEditorChange = (newValue) => {
88 | if (newValue !== editorValue) {
89 | setEditorValue(newValue);
90 | }
91 | };
92 |
93 | return (
94 |
95 |
110 |
111 | );
112 | };
113 |
114 | export default MarkdownEditor;
115 |
--------------------------------------------------------------------------------
/src/components/GitHubActivity.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { months } from "../utils/dates";
3 | import { useGithub } from "../context/GithubProvider";
4 | // Fully styled GitHubActivity component
5 | const GitHubActivity = () => {
6 | const [contributionData, setContributionData] = useState(null);
7 | const [loading, setLoading] = useState(true);
8 | const [error, setError] = useState(null);
9 | const { username, token } = useGithub();
10 | const year = new Date().getFullYear();
11 | // Container ref for positioning
12 | const calendarRef = useRef(null);
13 |
14 | // Month labels
15 |
16 | // Function to determine color based on contribution count
17 | const getActivityColor = (count) => {
18 | if (count === 0) return "#ffffff10";
19 | if (count <= 4) return "#0e4429";
20 | if (count <= 8) return "#006d32";
21 | if (count <= 12) return "#26a641";
22 | return "#39d353";
23 | };
24 |
25 | // Fetch GitHub contribution data
26 | useEffect(() => {
27 | const fetchContributions = async () => {
28 | if (!username) {
29 | setLoading(false);
30 | setContributionData(generateSampleData());
31 | return;
32 | }
33 | setLoading(true);
34 | setError(null);
35 | try {
36 | // GitHub GraphQL API query to get contribution data
37 | const query = `
38 | query {
39 | user(login: "${username}") {
40 | contributionsCollection(from: "${year}-01-01T00:00:00Z", to: "${year}-12-31T23:59:59Z") {
41 | contributionCalendar {
42 | totalContributions
43 | weeks {
44 | firstDay
45 | contributionDays {
46 | date
47 | contributionCount
48 | weekday
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
55 | `;
56 |
57 | // Get GitHub token from props or environment
58 | const accessToken = token || process.env.REACT_APP_GITHUB_TOKEN;
59 |
60 | if (!accessToken) {
61 | console.warn(
62 | "No GitHub token provided. API calls may be rate-limited."
63 | );
64 | }
65 |
66 | const response = await fetch("https://api.github.com/graphql", {
67 | method: "POST",
68 | headers: {
69 | "Content-Type": "application/json",
70 | Authorization: accessToken ? `Bearer ${accessToken}` : "",
71 | },
72 | body: JSON.stringify({ query }),
73 | });
74 |
75 | const result = await response.json();
76 |
77 | if (result.errors) {
78 | throw new Error(result.errors[0].message);
79 | }
80 |
81 | if (!result.data || !result.data.user) {
82 | throw new Error(
83 | "User not found or API returned unexpected data structure"
84 | );
85 | }
86 |
87 | const userData =
88 | result.data.user.contributionsCollection.contributionCalendar;
89 |
90 | // Validate data structure
91 | if (!userData.weeks || !Array.isArray(userData.weeks)) {
92 | throw new Error("Invalid contribution data format");
93 | }
94 |
95 | setContributionData(userData);
96 | } catch (err) {
97 | console.error("Error fetching GitHub contributions:", err);
98 | setError(`Failed to fetch data: ${err.message}`);
99 | // Use sample data as fallback
100 | setContributionData(generateSampleData());
101 | } finally {
102 | setLoading(false);
103 | }
104 | };
105 |
106 | fetchContributions();
107 | }, [username, year, token]);
108 |
109 | // Generate sample data for preview or when API fails
110 | const generateSampleData = () => {
111 | const startDate = new Date(`${year}-01-01`);
112 | const endDate = new Date(`${year}-12-31`);
113 | const dayCount =
114 | Math.round((endDate - startDate) / (24 * 60 * 60 * 1000)) + 1;
115 |
116 | const weeks = [];
117 | let currentWeek = [];
118 | let currentDay = new Date(startDate);
119 |
120 | for (let i = 0; i < dayCount; i++) {
121 | if (currentDay.getDay() === 0 && currentWeek.length > 0) {
122 | weeks.push({
123 | firstDay: currentWeek[0].date,
124 | contributionDays: [...currentWeek],
125 | });
126 | currentWeek = [];
127 | }
128 |
129 | currentWeek.push({
130 | date: currentDay.toISOString().split("T")[0],
131 | contributionCount: Math.floor(Math.random() * 10),
132 | weekday: currentDay.getDay(),
133 | });
134 |
135 | currentDay.setDate(currentDay.getDate() + 1);
136 | }
137 |
138 | // Add the final week
139 | if (currentWeek.length > 0) {
140 | weeks.push({
141 | firstDay: currentWeek[0].date,
142 | contributionDays: [...currentWeek],
143 | });
144 | }
145 |
146 | return {
147 | totalContributions: weeks.reduce(
148 | (sum, week) =>
149 | sum +
150 | week.contributionDays.reduce(
151 | (weekSum, day) => weekSum + day.contributionCount,
152 | 0
153 | ),
154 | 0
155 | ),
156 | weeks,
157 | };
158 | };
159 |
160 | if (loading) {
161 | return (
162 |
163 |
164 |
165 | );
166 | }
167 |
168 | if (error && !contributionData) {
169 | return (
170 |
171 |
Error loading GitHub contributions
172 |
{error}
173 |
174 | );
175 | }
176 |
177 | // Generate the calendar grid with proper week/day alignment
178 | const renderCalendar = () => {
179 | if (
180 | !contributionData ||
181 | !contributionData.weeks ||
182 | contributionData.weeks.length === 0
183 | ) {
184 | return (
185 |
186 | No contribution data available
187 |
188 | );
189 | }
190 |
191 | // Generate month labels
192 | const monthLabels = [];
193 | let currentMonth = -1;
194 |
195 | contributionData.weeks.forEach((week, weekIndex) => {
196 | // Check if firstDay is valid
197 | if (!week.firstDay) return;
198 |
199 | const date = new Date(week.firstDay);
200 | const month = date.getMonth();
201 |
202 | if (month !== currentMonth) {
203 | currentMonth = month;
204 | monthLabels.push(
205 |
210 | {months[month].slice(0, 3)}
211 |
212 | );
213 | }
214 | });
215 |
216 | // Generate cells for each contribution day
217 | const cells = [];
218 |
219 | contributionData.weeks.forEach((week, weekIndex) => {
220 | // Some weeks might not have all days, especially at the beginning/end of year
221 | if (!week.contributionDays || !Array.isArray(week.contributionDays))
222 | return;
223 |
224 | week.contributionDays.forEach((day) => {
225 | // Make sure all required fields exist
226 | if (!day.date || day.contributionCount === undefined) return;
227 |
228 | const date = new Date(day.date);
229 | const formattedDate = `${date.toLocaleString("default", {
230 | month: "short",
231 | })} ${date.getDate()}, ${date.getFullYear()}`;
232 | const cellTooltip = `${day.contributionCount} contrb - ${formattedDate}`;
233 |
234 | // Use the weekday from the API if available, or calculate it
235 | const dayIndex =
236 | day.weekday !== undefined ? day.weekday : date.getDay();
237 |
238 | cells.push(
239 |
251 | );
252 | });
253 | });
254 |
255 | return (
256 |
257 |
{monthLabels}
258 |
{cells}
259 |
260 | );
261 | };
262 |
263 | // Render the calendar, tooltip, and stats
264 | return (
265 |
266 |
267 | Total: {contributionData?.totalContributions || 0}
268 |
269 |
270 |
{renderCalendar()}
271 |
272 | );
273 | };
274 |
275 | export default GitHubActivity;
276 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "JetBrains";
3 | src: url("/fonts/JetBrainsMono-Medium.ttf") format("truetype");
4 | font-weight: normal;
5 | font-style: normal;
6 | }
7 |
8 | @font-face {
9 | font-family: "Montserrat";
10 | src: url("/fonts/Montserrat-Medium.ttf") format("truetype");
11 | font-weight: normal;
12 | font-style: normal;
13 | }
14 |
15 | @font-face {
16 | font-family: "Montserrat";
17 | src: url("/fonts/Montserrat-Bold.ttf") format("truetype");
18 | font-weight: bold;
19 | font-style: normal;
20 | }
21 |
22 | @font-face {
23 | font-family: "Montserrat";
24 | src: url("/fonts/Montserrat-BoldItalic.ttf") format("truetype");
25 | font-weight: bold;
26 | font-style: italic;
27 | }
28 | @font-face {
29 | font-family: "Montserrat";
30 | src: url("/fonts/Montserrat-MediumItalic.ttf") format("truetype");
31 | font-weight: normal;
32 | font-style: italic;
33 | }
34 |
35 | * {
36 | padding: 0;
37 | margin: 0;
38 | box-sizing: border-box;
39 | color: #fff;
40 | -ms-overflow-style: none;
41 | }
42 |
43 | ::-webkit-scrollbar {
44 | width: 0;
45 | display: none;
46 | }
47 |
48 | img {
49 | -webkit-user-drag: none;
50 | }
51 |
52 | html {
53 | background: #151515;
54 | overflow-x: hidden;
55 | }
56 |
57 | .drag-place {
58 | position: absolute;
59 | top: 0;
60 | left: 0;
61 | -webkit-app-region: drag;
62 | width: 100%;
63 | height: 35px;
64 | }
65 |
66 | .container {
67 | padding: 30px;
68 | max-width: 600px;
69 | margin: auto;
70 | }
71 |
72 | .spinner {
73 | width: 25px;
74 | height: 25px;
75 | border: 2px solid transparent;
76 | border-top-color: #ffffff;
77 | border-left-color: #ffffff;
78 | border-bottom-color: #ffffff;
79 | border-radius: 50%;
80 | animation: spin 0.8s linear infinite;
81 | }
82 |
83 | @keyframes spin {
84 | 0% {
85 | transform: rotate(0deg);
86 | }
87 | 100% {
88 | transform: rotate(360deg);
89 | }
90 | }
91 |
92 | .topbar-container {
93 | display: flex;
94 | align-items: start;
95 | justify-content: space-between;
96 | max-width: 600px;
97 | }
98 |
99 | .topbar-wrapper {
100 | display: flex;
101 | align-items: center;
102 | gap: 5px;
103 | height: 70px;
104 | user-select: none;
105 | font-family: JetBrains;
106 | }
107 |
108 | .topbar-wrapper .day {
109 | font-size: 70px;
110 | }
111 |
112 | .topbar-wrapper .months-names {
113 | display: flex;
114 | flex-direction: column;
115 | margin-top: 5px;
116 | }
117 |
118 | .settings-button {
119 | background: transparent;
120 | border: none;
121 | outline: none;
122 | height: 30px;
123 | width: 30px;
124 | display: flex;
125 | align-items: center;
126 | justify-content: center;
127 | opacity: 0.3;
128 | cursor: pointer;
129 | }
130 |
131 | .months-names p {
132 | font-size: 20px;
133 | margin-bottom: -5px;
134 | }
135 |
136 | .showcase-header {
137 | display: flex;
138 | align-items: center;
139 | justify-content: space-between;
140 | }
141 |
142 | .showcase-title {
143 | opacity: 50%;
144 | display: flex;
145 | align-items: center;
146 | gap: 3px;
147 | }
148 |
149 | .new-button {
150 | background: transparent;
151 | padding: 3px 10px;
152 | cursor: pointer;
153 | border: none;
154 | outline: none;
155 | }
156 |
157 | .showcase-wrapper {
158 | padding: 10px;
159 | max-height: 250px;
160 | overflow-y: auto;
161 | }
162 |
163 | .showcase-title .title {
164 | font-family: JetBrains;
165 | }
166 |
167 | .showcase-link {
168 | display: flex;
169 | align-items: center;
170 | gap: 15px;
171 | padding: 5px 10px;
172 | border-radius: 5px;
173 | cursor: pointer;
174 | }
175 |
176 | .showcase-link-title {
177 | display: flex;
178 | align-items: center;
179 | width: 100%;
180 | justify-content: space-between;
181 | }
182 |
183 | .showcase-link-title h3 {
184 | max-width: 90%;
185 | }
186 |
187 | .showcase-link:hover {
188 | background-color: #ffffff09;
189 | }
190 |
191 | .showcase-link img {
192 | opacity: 80%;
193 | }
194 |
195 | .showcase-link h3 {
196 | font-family: Montserrat;
197 | font-weight: normal;
198 | padding-bottom: 1px;
199 | }
200 |
201 | .showcase-link.add {
202 | padding: 10px;
203 | opacity: 50%;
204 | }
205 |
206 | .mdx-wrapper {
207 | display: none;
208 | opacity: 0;
209 | min-height: 465px;
210 | transition: all 200ms ease;
211 | cursor: text;
212 | margin-top: -15px;
213 | }
214 |
215 | .mdx-editor {
216 | min-height: 465px;
217 | }
218 |
219 | .mdx-editor,
220 | .mdx-editor > * {
221 | border: none;
222 | outline: none;
223 | caret-color: #fff;
224 | font-family: Montserrat;
225 | font-weight: normal;
226 | }
227 |
228 | .mdx-editor blockquote,
229 | blockquote {
230 | padding: 5px 15px;
231 | border-left: 2px solid #fff;
232 | background: #ffffff11;
233 | margin: 5px 0;
234 | }
235 |
236 | .mdx-editor p a span,
237 | .mdx-editor p a {
238 | color: #00a2ff;
239 | }
240 |
241 | .mdx-editor h2:has(code),
242 | .mdx-editor h3:has(code),
243 | .mdx-editor h4:has(code),
244 | .mdx-editor h5:has(code),
245 | .mdx-editor h6:has(code) {
246 | margin: 1px 0;
247 | }
248 |
249 | code span {
250 | font-family: JetBrains;
251 | border-radius: 4px;
252 | background: transparent !important;
253 | border: 1px solid #ffffff30;
254 | }
255 |
256 | ._linkDialogPopoverContent_uazmk_600 {
257 | background: #181818 !important;
258 | border: 1px solid #ffffff10 !important;
259 | padding: 3px 10px !important;
260 | }
261 |
262 | ._linkDialogPopoverContent_uazmk_600 button:hover {
263 | background: #191919 !important;
264 | cursor: pointer;
265 | }
266 |
267 | ._linkDialogPopoverContent_uazmk_600 input {
268 | background: transparent !important;
269 | border: 1px solid #ffffff11;
270 | }
271 |
272 | ._linkDialogPopoverContent_uazmk_600 form button {
273 | background: #ffffff08 !important;
274 | border: none;
275 | }
276 |
277 | ._linkDialogPopoverContent_uazmk_600 form button:hover {
278 | background: #ffffff15 !important;
279 | }
280 |
281 | ._listItemUnchecked_1tncs_74::before {
282 | border-radius: 4px !important;
283 | }
284 |
285 | ._listItemChecked_1tncs_73::before {
286 | border-radius: 4px !important;
287 | background-color: white !important;
288 | border: none !important;
289 | height: 18px !important;
290 | width: 18px !important;
291 | }
292 |
293 | ._listItemChecked_1tncs_73::after {
294 | top: 3px !important;
295 | border-width: 0 1.5px 1.5px 0 !important;
296 | border-color: black !important;
297 | }
298 |
299 | ._listItemUnchecked_1tncs_74:focus::before,
300 | ._listItemChecked_1tncs_73:focus::before {
301 | box-shadow: none !important;
302 | border-radius: 4px !important;
303 | }
304 |
305 | .bottom-bar {
306 | transition: all 200ms ease;
307 | }
308 |
309 | .footbar {
310 | position: absolute;
311 | top: 93%;
312 | left: 20px;
313 | width: 540px;
314 | display: flex;
315 | align-items: center;
316 | justify-content: space-between;
317 | }
318 |
319 | .folder-links-wrapper {
320 | display: flex;
321 | align-items: center;
322 | }
323 |
324 | .folder-links-wrapper h4 {
325 | font-family: JetBrains;
326 | }
327 |
328 | .folder-link {
329 | display: flex;
330 | align-items: center;
331 | gap: 5px;
332 | opacity: 30%;
333 | padding: 2px 10px;
334 | background: transparent;
335 | border: none;
336 | outline: none;
337 | cursor: pointer;
338 | transition: opacity 300ms ease;
339 | }
340 |
341 | .folder-link.active {
342 | opacity: 50%;
343 | }
344 |
345 | .about-btn {
346 | background: transparent;
347 | border: none;
348 | outline: none;
349 | opacity: 0.2;
350 | margin-right: -10px;
351 | cursor: pointer;
352 | }
353 |
354 | .about-page {
355 | position: fixed;
356 | top: 0;
357 | left: 0;
358 | height: 100vh;
359 | width: 100%;
360 | background: #151515aa;
361 | backdrop-filter: blur(30px);
362 | font-family: Montserrat;
363 | font-weight: normal;
364 | font-style: normal;
365 | display: flex;
366 | flex-direction: column;
367 | opacity: 0;
368 | transform: translateX(-20px);
369 | transition: all 200ms ease;
370 | visibility: hidden;
371 | overflow-y: auto;
372 | scroll-snap-type: y mandatory;
373 | scroll-behavior: smooth;
374 | }
375 |
376 | .about-page .section {
377 | display: flex;
378 | flex-direction: column;
379 | gap: 20px;
380 | min-height: 100vh;
381 | padding: 30px;
382 | scroll-snap-align: start;
383 | overflow-y: auto;
384 | }
385 |
386 | .about-page.visible {
387 | opacity: 1;
388 | transform: translateX(0);
389 | visibility: visible;
390 | }
391 |
392 | .section .title {
393 | display: flex;
394 | align-items: center;
395 | }
396 |
397 | .section .title img {
398 | height: 40px;
399 | width: 50px;
400 | object-fit: cover;
401 | }
402 |
403 | .privacy .action-list {
404 | display: flex;
405 | flex-direction: column;
406 | gap: 30px;
407 | }
408 |
409 | .privacy .option {
410 | display: flex;
411 | flex-direction: column;
412 | align-items: flex-start;
413 | gap: 6px;
414 | }
415 |
416 | .actions-list {
417 | display: flex;
418 | flex-direction: column;
419 | gap: 30px;
420 | }
421 |
422 | .action {
423 | display: flex;
424 | flex-direction: column;
425 | gap: 10px;
426 | }
427 |
428 | .action div:nth-child(1) h3 {
429 | margin-bottom: 10px;
430 | font-weight: 400;
431 | }
432 |
433 | .option {
434 | display: flex;
435 | align-items: center;
436 | gap: 6px;
437 | }
438 |
439 | .option.lists {
440 | flex-direction: column;
441 | align-items: start;
442 | }
443 |
444 | .option > * {
445 | font-weight: normal;
446 | }
447 |
448 | .option em {
449 | font-style: italic;
450 | }
451 |
452 | .action span {
453 | padding: 1px 4px;
454 | border-radius: 4px;
455 | background: transparent !important;
456 | border: 1px solid #ffffff30;
457 | }
458 |
459 | .action .checkbox {
460 | background: transparent;
461 | cursor: pointer;
462 | margin-bottom: 5px;
463 | }
464 |
465 | .action .checkbox[type="checkbox"]::before {
466 | content: "";
467 | position: relative;
468 | display: block;
469 | width: 16px;
470 | height: 16px;
471 | border: 1px solid #fff;
472 | background: #151515;
473 | border-radius: 4px;
474 | }
475 |
476 | .action .checkbox[type="checkbox"]:checked:before {
477 | border: none;
478 | background: #fff;
479 | width: 18px;
480 | height: 18px;
481 | }
482 |
483 | .action .checkbox[type="checkbox"]::after {
484 | content: "";
485 | position: relative;
486 | display: block;
487 | left: 4px;
488 | top: -13px;
489 | height: 0.25rem;
490 | width: 0.5rem;
491 | background: transparent;
492 | border: 2px solid #000;
493 | border-width: 0 0 1.5px 1.5px;
494 | rotate: -45deg;
495 | }
496 |
497 | .settings-container {
498 | position: absolute;
499 | top: 10px;
500 | background: #11111170;
501 | border: 1px solid #ffffff10;
502 | height: 580px;
503 | width: 400px;
504 | padding: 20px 25px;
505 | border-radius: 10px;
506 | backdrop-filter: blur(20px);
507 | transition: right 400ms ease;
508 | z-index: 10;
509 | font-family: Montserrat;
510 | font-weight: normal;
511 | display: flex;
512 | flex-direction: column;
513 | }
514 |
515 | .titlebar {
516 | display: flex;
517 | align-items: center;
518 | justify-content: space-between;
519 | }
520 |
521 | .titlebar h2,
522 | .settings-item h4 {
523 | font-weight: normal;
524 | }
525 |
526 | .titlebar img {
527 | rotate: 45deg;
528 | }
529 |
530 | .titlebar .close-btn {
531 | background: transparent;
532 | height: 30px;
533 | width: 30px;
534 | display: grid;
535 | place-items: center;
536 | border: none;
537 | outline: none;
538 | cursor: pointer;
539 | opacity: 0.4;
540 | }
541 |
542 | .settings-items {
543 | padding: 20px 0;
544 | display: flex;
545 | flex-direction: column;
546 | gap: 10px;
547 | }
548 |
549 | .settings-item {
550 | display: flex;
551 | flex-direction: column;
552 | gap: 10px;
553 | }
554 |
555 | .settings-field {
556 | width: 100%;
557 | max-width: 400px;
558 | padding: 10px 14px;
559 | font-size: 14px;
560 | background: transparent;
561 | border: 1px solid #444;
562 | border-radius: 8px;
563 | outline: none;
564 | transition: all 0.3s ease;
565 | resize: none;
566 | }
567 |
568 | textarea.settings-field {
569 | height: 75px;
570 | }
571 |
572 | .settings-field:focus {
573 | border-color: #ffffff80;
574 | }
575 |
576 | .settings-field::placeholder {
577 | color: #ffffff80;
578 | }
579 |
580 | .version {
581 | align-self: self-end;
582 | margin-top: auto;
583 | font-family: JetBrains;
584 | font-size: 14px;
585 | opacity: 0.4;
586 | user-select: none;
587 | }
588 |
589 | /* ======== GITHUB ACTIVITY Styles */
590 |
591 |
592 | .github-calendar {
593 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
594 | sans-serif;
595 | padding: 10px 15px;
596 | width: 100%;
597 | height: 170px;
598 | max-width: 900px;
599 | box-sizing: border-box;
600 | margin: 0 auto;
601 | margin-bottom: 5px;
602 | }
603 |
604 | .github-calendar-stats {
605 | font-size: 14px;
606 | margin-bottom: 15px;
607 | color: #8b949e;
608 | font-family: JetBrains;
609 | }
610 |
611 | .github-calendar-wrapper {
612 | overflow-x: auto;
613 | }
614 |
615 | .github-calendar-container {
616 | position: relative;
617 | padding-top: 20px;
618 | }
619 |
620 | .github-calendar-months {
621 | position: relative;
622 | margin-bottom: 5px;
623 | }
624 |
625 | .github-calendar-month {
626 | position: absolute;
627 | top: -20px;
628 | font-size: 10px;
629 | color: #586069;
630 | }
631 |
632 | .github-calendar-grid {
633 | position: relative;
634 | height: 100px;
635 | }
636 |
637 | .github-calendar-cell {
638 | width: 10px;
639 | height: 10px;
640 | margin: 1px;
641 | position: absolute;
642 | border-radius: 2px;
643 | transition: transform 0.1s ease-in-out;
644 | }
645 |
646 | .github-calendar-legend {
647 | margin-top: 15px;
648 | display: flex;
649 | align-items: center;
650 | justify-content: center;
651 | gap: 5px;
652 | font-size: 12px;
653 | color: #586069;
654 | }
655 |
656 | .github-calendar-legend-item {
657 | width: 10px;
658 | height: 10px;
659 | border-radius: 2px;
660 | }
661 |
662 | .github-calendar-loading {
663 | height: 170px;
664 | display: flex;
665 | align-items: center;
666 | justify-content: center;
667 | margin-bottom: 5px;
668 | }
669 |
670 | .spinner-circle {
671 | width: 25px;
672 | height: 25px;
673 | border: 2px solid transparent;
674 | border-top-color: #ffffff;
675 | border-left-color: #ffffff;
676 | border-bottom-color: #ffffff;
677 | border-radius: 50%;
678 | animation: spin 0.8s linear infinite;
679 | }
680 |
681 | @keyframes spin {
682 | 0% {
683 | transform: rotate(0deg);
684 | }
685 | 100% {
686 | transform: rotate(360deg);
687 | }
688 | }
689 |
690 | .github-calendar-error {
691 | padding: 15px;
692 | color: #cb2431;
693 | border: 1px solid #f97583;
694 | border-radius: 6px;
695 | background-color: #ffeef0;
696 | margin-bottom: 15px;
697 | }
698 |
699 | .github-calendar-error h3 {
700 | margin-top: 0;
701 | font-size: 16px;
702 | }
703 |
704 | /* Support dark mode if the user's system prefers it */
705 |
706 | .github-calendar-header h2 {
707 | color: #c9d1d9;
708 | }
709 |
710 | .github-calendar-month,
711 | .github-calendar-day {
712 | color: #8b949e;
713 | }
714 |
715 | .github-calendar-legend {
716 | color: #8b949e;
717 | }
718 |
719 | .github-calendar-cell[data-count="0"] {
720 | background-color: #1d1d1d !important;
721 | }
722 |
723 | .github-calendar-loading,
724 | .github-calendar-empty {
725 | color: #8b949e;
726 | }
727 |
--------------------------------------------------------------------------------