diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0596f8b4..31a661b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,6 @@ Scan through our [existing issues](https://github.com/its-me-abhishek/ccsync/iss 1. Fork the repository. - Using GitHub Desktop: - - [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop. - Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)! diff --git a/development/README.md b/development/README.md index 892912a5..4937320f 100644 --- a/development/README.md +++ b/development/README.md @@ -6,7 +6,6 @@ The `setup.sh` script starts all three services (backend, frontend, and sync ser > **Note:** The backend should ideally be run in a separate user environment (preferably root user) to avoid permission issues with Taskwarrior configuration files. - > **Git Hooks:** Pre-commit hooks are automatically configured when you run `npm install` in the frontend directory. These hooks will format your code before each commit. ## Prerequisites diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8cf17b73..823554c1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -97,6 +97,7 @@ "dlv": "^1.1.3", "doctrine": "^3.0.0", "dom-accessibility-api": "^0.5.16", + "driver.js": "^1.3.1", "electron-to-chromium": "^1.4.796", "emittery": "^0.13.1", "emoji-regex": "^8.0.0", @@ -460,6 +461,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -2064,6 +2066,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -5170,7 +5173,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5186,7 +5188,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5203,7 +5204,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -5216,7 +5216,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -5226,7 +5225,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5423,8 +5421,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5578,6 +5575,7 @@ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, + "peer": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -5647,6 +5645,7 @@ "version": "20.14.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -5667,6 +5666,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", "devOptional": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5686,6 +5686,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "devOptional": true, + "peer": true, "dependencies": { "@types/react": "*" } @@ -5777,6 +5778,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6252,6 +6254,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -7084,6 +7087,12 @@ "node": ">=12" } }, + "node_modules/driver.js": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.3.6.tgz", + "integrity": "sha512-g2nNuu+tWmPpuoyk3ffpT9vKhjPz4NrJzq6mkRDZIwXCrFhrKdDJ9TX5tJOBpvCTBrBYjgRQ17XlcQB15q4gMg==", + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7228,6 +7237,7 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8288,6 +8298,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -10679,6 +10690,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -10942,6 +10954,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10965,6 +10978,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10998,12 +11012,14 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -11203,7 +11219,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -11798,6 +11815,7 @@ "version": "3.4.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -11966,6 +11984,7 @@ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.4.tgz", "integrity": "sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==", "dev": true, + "peer": true, "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", @@ -12022,6 +12041,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12105,6 +12125,7 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12329,6 +12350,7 @@ "version": "5.2.13", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", + "peer": true, "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", diff --git a/frontend/package.json b/frontend/package.json index 0241e8e4..03ca210a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -91,6 +91,7 @@ "dlv": "^1.1.3", "doctrine": "^3.0.0", "dom-accessibility-api": "^0.5.16", + "driver.js": "^1.3.1", "electron-to-chromium": "^1.4.796", "emittery": "^0.13.1", "emoji-regex": "^8.0.0", diff --git a/frontend/src/App.css b/frontend/src/App.css index 0a7574d4..4fc8e5d4 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -80,3 +80,79 @@ ::-webkit-scrollbar-thumb:hover { background: #3bbcf0; } + +.driver-popover.ccsync-tour-popover { + background-color: hsl(var(--card)); + color: hsl(var(--card-foreground)); + border-radius: 0.75rem; + border: 1px solid hsl(var(--border)); + /* box-shadow: 0 20px 45px rgba(15, 23, 42, 0.18); */ + max-width: 320px; + padding: 1rem 1.25rem 1.1rem 1.25rem; +} + +.driver-popover.ccsync-tour-popover .driver-popover-title { + font-size: 1rem; + font-weight: 600; +} + +.driver-popover.ccsync-tour-popover .driver-popover-description { + color: hsl(var(--muted-foreground)); + line-height: 1.55; + font-size: 0.92rem; +} + +.driver-popover.ccsync-tour-popover .driver-popover-progress-text { + color: hsl(var(--muted-foreground)); + font-size: 0.75rem; +} + +.driver-popover.ccsync-tour-popover .driver-popover-navigation-btns { + gap: 0.5rem; +} + +.driver-popover.ccsync-tour-popover .driver-popover-navigation-btns button { + border-radius: 9999px; + border: 1px solid transparent; + padding: 0.35rem 0.85rem; + font-weight: 500; + cursor: pointer; + text-shadow: none; +} + +.driver-popover.ccsync-tour-popover + .driver-popover-navigation-btns + button.driver-popover-next-btn, +.driver-popover.ccsync-tour-popover + .driver-popover-navigation-btns + button.driver-popover-done-btn { + background-color: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} + +.driver-popover.ccsync-tour-popover + .driver-popover-navigation-btns + button.driver-popover-prev-btn { + background-color: hsl(var(--secondary)); + color: hsl(var(--secondary-foreground)); +} + +.driver-popover.ccsync-tour-popover + .driver-popover-navigation-btns + button.driver-popover-prev-btn:hover { + background-color: hsl(var(--accent)); +} + +.driver-popover.ccsync-tour-popover .driver-skip-btn { + background-color: transparent; + color: hsl(var(--muted-foreground)); + border: none; + padding: 0.35rem 0.6rem; + font-weight: 500; + cursor: pointer; + text-shadow: none; +} + +.driver-popover.ccsync-tour-popover .driver-skip-btn:hover { + color: hsl(var(--foreground)); +} diff --git a/frontend/src/components/HomePage.tsx b/frontend/src/components/HomePage.tsx index 3b40f839..bbb68b1d 100644 --- a/frontend/src/components/HomePage.tsx +++ b/frontend/src/components/HomePage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Navbar } from './HomeComponents/Navbar/Navbar'; import { Hero } from './HomeComponents/Hero/Hero'; import { Footer } from './HomeComponents/Footer/Footer'; @@ -11,6 +11,8 @@ import { motion } from 'framer-motion'; import { toast } from 'react-toastify'; import { Task } from '@/components/utils/types'; import { fetchTaskwarriorTasks } from './HomeComponents/Tasks/hooks'; +import { driver } from 'driver.js'; +import 'driver.js/dist/driver.css'; export const HomePage: React.FC = () => { const [userInfo, setUserInfo] = useState(null); @@ -18,6 +20,8 @@ export const HomePage: React.FC = () => { const navigate = useNavigate(); const [tasks, setTasks] = useState(null); + const hasTourStartedRef = useRef(false); + const tourTimeoutRef = useRef(null); const getTasks = async ( email: string, @@ -44,6 +48,7 @@ export const HomePage: React.FC = () => { } }; + // Launch onboarding tour for new HomePage visitors. useEffect(() => { fetchUserInfo(); }, []); @@ -142,6 +147,134 @@ export const HomePage: React.FC = () => { }; }, [userInfo]); + useEffect(() => { + if (!userInfo?.email) { + return; + } + + if (hasTourStartedRef.current) { + return; + } + + if (typeof window === 'undefined') { + return; + } + + const tourStorageKey = `ccsync-home-tour-${userInfo.email}`; + if (window.localStorage.getItem(tourStorageKey) === 'seen') { + hasTourStartedRef.current = true; + return; + } + + const markTourSeen = () => { + window.localStorage.setItem(tourStorageKey, 'seen'); + }; + + const driverInstance = driver({ + popoverClass: 'ccsync-tour-popover', + showProgress: true, + overlayOpacity: 0.45, + stagePadding: 8, + allowClose: true, + showButtons: ['previous', 'next', 'close'], + nextBtnText: 'Next', + prevBtnText: 'Back', + doneBtnText: 'Finish', + steps: [ + { + element: '#home-navbar', + popover: { + title: 'Navigation hub', + description: + 'Find task actions, logs, and your account controls from the top bar.', + side: 'bottom', + align: 'center', + }, + }, + { + element: '#home-hero', + popover: { + title: userInfo.name + ? `Welcome, ${userInfo.name.split(' ')[0]}!` + : 'Welcome to CCSync', + description: + 'Kick off sync jobs, copy credentials, and review your Taskwarrior status from here.', + side: 'bottom', + align: 'start', + }, + }, + { + element: '#home-tasks', + popover: { + title: 'Live task board', + description: + 'View, edit, complete, or delete Taskwarrior items and watch updates stream in real time.', + side: 'top', + align: 'start', + }, + }, + { + element: '#home-setup-guide', + popover: { + title: 'Setup guide', + description: + 'Follow these steps to connect Taskwarrior and keep CCSync working across your devices.', + side: 'top', + align: 'start', + }, + }, + { + element: '#home-faq', + popover: { + title: 'Need help?', + description: + 'The FAQ covers common troubleshooting tips. Reach out if you still need a hand.', + side: 'top', + align: 'start', + }, + }, + ], + onDestroyed: () => { + markTourSeen(); + }, + onCloseClick: () => { + markTourSeen(); + driverInstance.destroy(); + }, + onPopoverRender: (popover) => { + if (!popover.footerButtons.querySelector('[data-driver-skip-button]')) { + const skipButton = document.createElement('button'); + skipButton.type = 'button'; + skipButton.textContent = 'Skip'; + skipButton.dataset.driverSkipButton = 'true'; + skipButton.className = 'driver-skip-btn'; + skipButton.addEventListener('click', () => { + markTourSeen(); + driverInstance.destroy(); + }); + popover.footerButtons.prepend(skipButton); + } + }, + }); + + hasTourStartedRef.current = true; + + tourTimeoutRef.current = window.setTimeout(() => { + driverInstance.drive(); + }, 600); + + return () => { + if (tourTimeoutRef.current) { + window.clearTimeout(tourTimeoutRef.current); + tourTimeoutRef.current = null; + } + + if (driverInstance.isActive()) { + driverInstance.destroy(); + } + }; + }, [userInfo]); + const fetchUserInfo = async () => { try { const response = await fetch(url.backendURL + 'api/user', { @@ -165,17 +298,20 @@ export const HomePage: React.FC = () => {
{userInfo ? (
- +
+ +
{ encryption_secret={userInfo.encryption_secret} /> - - - +
+ +
+
+ +
+
+ +
) : (