video lesson tracking for courses, new userlesson table, also github experiment

This commit is contained in:
austinkelsay 2024-09-19 15:38:36 -05:00
parent 2e25beea71
commit f7bbf93f95
15 changed files with 952 additions and 130 deletions

326
package-lock.json generated
View File

@ -17,6 +17,9 @@
"@next-auth/prisma-adapter": "^1.0.7",
"@nostr-dev-kit/ndk": "^2.10.0",
"@nostr-dev-kit/ndk-cache-dexie": "^2.5.1",
"@octokit/plugin-retry": "^5.0.0",
"@octokit/plugin-throttling": "^6.0.0",
"@octokit/rest": "^19.0.7",
"@prisma/client": "^5.17.0",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/react-query": "^5.51.21",
@ -24,6 +27,7 @@
"@uiw/react-md-editor": "^3.11.0",
"axios": "^1.7.2",
"bech32": "^2.0.0",
"chart.js": "^4.4.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cors": "^2.8.5",
@ -1838,6 +1842,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==",
"license": "MIT"
},
"node_modules/@lightninglabs/lnc-core": {
"version": "0.3.1-alpha",
"resolved": "https://registry.npmjs.org/@lightninglabs/lnc-core/-/lnc-core-0.3.1-alpha.tgz",
@ -2173,6 +2183,253 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@octokit/auth-token": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz",
"integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/core": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.4.tgz",
"integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^3.0.0",
"@octokit/graphql": "^5.0.0",
"@octokit/request": "^6.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/endpoint": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz",
"integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/graphql": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz",
"integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^9.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/openapi-types": {
"version": "18.1.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz",
"integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz",
"integrity": "sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==",
"license": "MIT",
"dependencies": {
"@octokit/tsconfig": "^1.0.2",
"@octokit/types": "^9.2.3"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=4"
}
},
"node_modules/@octokit/plugin-request-log": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
"license": "MIT",
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz",
"integrity": "sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^10.0.0"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
"integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^18.0.0"
}
},
"node_modules/@octokit/plugin-retry": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
"integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
"license": "MIT",
"dependencies": {
"@octokit/request-error": "^4.0.1",
"@octokit/types": "^10.0.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/request-error": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-4.0.2.tgz",
"integrity": "sha512-uqwUEmZw3x4I9DGYq9fODVAAvcLsPQv97NRycP6syEFu5916M189VnNBW2zANNwqg3OiligNcAey7P0SET843w==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^10.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/types": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
"integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^18.0.0"
}
},
"node_modules/@octokit/plugin-throttling": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-6.1.0.tgz",
"integrity": "sha512-JqMbTiPC0sUSTsLQsdq3JVx1mx8UtTo5mwR80YqPXE93+XhevvSyOR1rO2Z+NbO/r0TK4hqFJSSi/9oIZBxZTg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^9.0.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "^4.0.0"
}
},
"node_modules/@octokit/request": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz",
"integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/request-error": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz",
"integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/request/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@octokit/rest": {
"version": "19.0.13",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.13.tgz",
"integrity": "sha512-/EzVox5V9gYGdbAI+ovYj3nXQT1TtTHRT+0eZPcuC05UFSWO3mdO9UY1C0i2eLF9Un1ONJkAk+IEtYGAC+TahA==",
"license": "MIT",
"dependencies": {
"@octokit/core": "^4.2.1",
"@octokit/plugin-paginate-rest": "^6.1.2",
"@octokit/plugin-request-log": "^1.0.4",
"@octokit/plugin-rest-endpoint-methods": "^7.1.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/tsconfig": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@octokit/tsconfig/-/tsconfig-1.0.2.tgz",
"integrity": "sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==",
"license": "MIT"
},
"node_modules/@octokit/types": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz",
"integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^18.0.0"
}
},
"node_modules/@panva/hkdf": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
@ -5442,6 +5699,12 @@
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==",
"license": "MIT"
},
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"license": "Apache-2.0"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -5460,6 +5723,12 @@
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bottleneck": {
"version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
"integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
"license": "MIT"
},
"node_modules/bowser": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
@ -5678,6 +5947,18 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chart.js": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz",
"integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -6108,6 +6389,12 @@
"node": ">=0.4.0"
}
},
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"license": "ISC"
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -8780,6 +9067,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@ -10774,7 +11070,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@ -13068,6 +13363,12 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@ -13449,6 +13750,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
@ -13690,6 +13997,12 @@
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/webpack": {
"version": "5.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
@ -13831,6 +14144,16 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -14037,7 +14360,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ws": {

View File

@ -18,6 +18,9 @@
"@next-auth/prisma-adapter": "^1.0.7",
"@nostr-dev-kit/ndk": "^2.10.0",
"@nostr-dev-kit/ndk-cache-dexie": "^2.5.1",
"@octokit/plugin-retry": "^5.0.0",
"@octokit/plugin-throttling": "^6.0.0",
"@octokit/rest": "^19.0.7",
"@prisma/client": "^5.17.0",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/react-query": "^5.51.21",
@ -25,6 +28,7 @@
"@uiw/react-md-editor": "^3.11.0",
"axios": "^1.7.2",
"bech32": "^2.0.0",
"chart.js": "^4.4.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cors": "^2.8.5",

View File

@ -66,57 +66,6 @@ CREATE TABLE "Role" (
CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Purchase" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"courseId" TEXT,
"resourceId" TEXT,
"amountPaid" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Lesson" (
"id" TEXT NOT NULL,
"courseId" TEXT,
"resourceId" TEXT,
"draftId" TEXT,
"index" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Lesson_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DraftLesson" (
"id" TEXT NOT NULL,
"courseDraftId" TEXT NOT NULL,
"resourceId" TEXT,
"draftId" TEXT,
"index" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DraftLesson_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Course" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"price" INTEGER NOT NULL DEFAULT 0,
"noteId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Course_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Resource" (
"id" TEXT NOT NULL,
@ -148,6 +97,18 @@ CREATE TABLE "Draft" (
CONSTRAINT "Draft_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Course" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"price" INTEGER NOT NULL DEFAULT 0,
"noteId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Course_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CourseDraft" (
"id" TEXT NOT NULL,
@ -163,6 +124,60 @@ CREATE TABLE "CourseDraft" (
CONSTRAINT "CourseDraft_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Lesson" (
"id" TEXT NOT NULL,
"courseId" TEXT,
"resourceId" TEXT,
"draftId" TEXT,
"index" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Lesson_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DraftLesson" (
"id" TEXT NOT NULL,
"courseDraftId" TEXT NOT NULL,
"resourceId" TEXT,
"draftId" TEXT,
"index" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DraftLesson_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserLesson" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"lessonId" TEXT NOT NULL,
"opened" BOOLEAN NOT NULL DEFAULT false,
"completed" BOOLEAN NOT NULL DEFAULT false,
"openedAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserLesson_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Purchase" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"courseId" TEXT,
"resourceId" TEXT,
"amountPaid" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey");
@ -187,11 +202,14 @@ CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provi
-- CreateIndex
CREATE UNIQUE INDEX "Role_userId_key" ON "Role"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId");
-- CreateIndex
CREATE UNIQUE INDEX "Course_noteId_key" ON "Course"("noteId");
-- CreateIndex
CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId");
CREATE UNIQUE INDEX "UserLesson_userId_lessonId_key" ON "UserLesson"("userId", "lessonId");
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@ -203,13 +221,16 @@ ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId"
ALTER TABLE "Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "Resource" ADD CONSTRAINT "Resource_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "Draft" ADD CONSTRAINT "Draft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "Course" ADD CONSTRAINT "Course_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Lesson" ADD CONSTRAINT "Lesson_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@ -230,13 +251,16 @@ ALTER TABLE "DraftLesson" ADD CONSTRAINT "DraftLesson_resourceId_fkey" FOREIGN K
ALTER TABLE "DraftLesson" ADD CONSTRAINT "DraftLesson_draftId_fkey" FOREIGN KEY ("draftId") REFERENCES "Draft"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Course" ADD CONSTRAINT "Course_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "UserLesson" ADD CONSTRAINT "UserLesson_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Resource" ADD CONSTRAINT "Resource_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "UserLesson" ADD CONSTRAINT "UserLesson_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "Lesson"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Draft" ADD CONSTRAINT "Draft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -27,6 +27,7 @@ model User {
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userLessons UserLesson[]
}
model Session {
@ -78,57 +79,6 @@ model Role {
nwc String?
}
model Purchase {
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id])
userId String
course Course? @relation(fields: [courseId], references: [id])
courseId String?
resource Resource? @relation(fields: [resourceId], references: [id])
resourceId String?
amountPaid Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Lesson {
id String @id @default(uuid())
courseId String?
course Course? @relation(fields: [courseId], references: [id])
resourceId String?
resource Resource? @relation(fields: [resourceId], references: [id])
draftId String?
draft Draft? @relation(fields: [draftId], references: [id])
index Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DraftLesson {
id String @id @default(uuid())
courseDraftId String
courseDraft CourseDraft @relation(fields: [courseDraftId], references: [id])
resourceId String?
resource Resource? @relation(fields: [resourceId], references: [id])
draftId String?
draft Draft? @relation(fields: [draftId], references: [id])
index Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Course {
id String @id
userId String
user User @relation(fields: [userId], references: [id])
price Int @default(0)
lessons Lesson[]
purchases Purchase[]
noteId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Resource {
id String @id // Client generates UUID
userId String
@ -162,6 +112,18 @@ model Draft {
lessons Lesson[]
}
model Course {
id String @id
userId String
user User @relation(fields: [userId], references: [id])
price Int @default(0)
lessons Lesson[]
purchases Purchase[]
noteId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CourseDraft {
id String @id @default(uuid())
userId String
@ -174,4 +136,60 @@ model CourseDraft {
topics String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Lesson {
id String @id @default(uuid())
courseId String?
course Course? @relation(fields: [courseId], references: [id])
resourceId String?
resource Resource? @relation(fields: [resourceId], references: [id])
draftId String?
draft Draft? @relation(fields: [draftId], references: [id])
index Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userLessons UserLesson[]
}
model DraftLesson {
id String @id @default(uuid())
courseDraftId String
courseDraft CourseDraft @relation(fields: [courseDraftId], references: [id])
resourceId String?
resource Resource? @relation(fields: [resourceId], references: [id])
draftId String?
draft Draft? @relation(fields: [draftId], references: [id])
index Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model UserLesson {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
lessonId String
lesson Lesson @relation(fields: [lessonId], references: [id])
opened Boolean @default(false)
completed Boolean @default(false)
openedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, lessonId])
}
model Purchase {
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id])
userId String
course Course? @relation(fields: [courseId], references: [id])
courseId String?
resource Resource? @relation(fields: [resourceId], references: [id])
resourceId String?
amountPaid Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@ -0,0 +1,77 @@
import React, { useState, useEffect, useCallback } from 'react';
import { getContributions } from '../../lib/github';
const GithubContributionChart = ({ username }) => {
const [contributionData, setContributionData] = useState({});
const [loading, setLoading] = useState(true);
const getColor = useCallback((count) => {
if (count === 0) return 'bg-gray-100';
if (count < 5) return 'bg-green-300';
if (count < 10) return 'bg-green-400';
if (count < 20) return 'bg-green-600';
return 'bg-green-700';
}, []);
const generateCalendar = useCallback(() => {
const today = new Date();
const sixMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1);
const calendar = [];
for (let d = new Date(sixMonthsAgo); d <= today; d.setDate(d.getDate() + 1)) {
const dateString = d.toISOString().split('T')[0];
const count = contributionData[dateString] || 0;
calendar.push({ date: new Date(d), count });
}
return calendar;
}, [contributionData]);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
await getContributions(username, (data) => {
setContributionData(data);
setLoading(false);
});
} catch (error) {
console.error("Error fetching contribution data:", error);
setLoading(false);
}
};
fetchData();
}, [username]);
const calendar = generateCalendar();
return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4">Github Contributions for {username}</h2>
{loading && <p>Loading contribution data...</p>}
<div className="flex flex-wrap gap-1">
{calendar.map((day, index) => (
<div
key={index}
className={`w-3 h-3 ${getColor(day.count)} rounded-sm cursor-pointer transition-all duration-200 ease-in-out hover:transform hover:scale-150`}
title={`${day.date.toDateString()}: ${day.count} contribution${day.count !== 1 ? 's' : ''}`}
></div>
))}
</div>
<div className="mt-2 text-sm text-gray-500 flex items-center">
<span className="mr-2">Less</span>
<div className="flex gap-1">
<div className="w-3 h-3 bg-gray-100 rounded-sm"></div>
<div className="w-3 h-3 bg-green-300 rounded-sm"></div>
<div className="w-3 h-3 bg-green-400 rounded-sm"></div>
<div className="w-3 h-3 bg-green-600 rounded-sm"></div>
<div className="w-3 h-3 bg-green-700 rounded-sm"></div>
</div>
<span className="ml-2">More</span>
</div>
</div>
);
};
export default GithubContributionChart;

View File

@ -136,14 +136,12 @@ export default function CourseDetailsNew({ processedEvent, paidCourse, lessons,
)}
</div>
</div>
<p className='text-xl text-gray-200 mb-4 mt-4 max-mob:text-base'>{processedEvent.description && (
<div className='mt-4'>
{processedEvent.description.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}
</div>
<div className='text-xl text-gray-200 mb-4 mt-4 max-mob:text-base'>{processedEvent.description && (
processedEvent.description.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))
)}
</p>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center'>
<Image
@ -172,11 +170,11 @@ export default function CourseDetailsNew({ processedEvent, paidCourse, lessons,
<div className='flex space-x-2 mt-4 sm:mt-0'>
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined />
<GenericButton outlined icon="pi pi-external-link" onClick={() => window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip={ isMobileView ? null : "View Nostr Event" } tooltipOptions={{ position: paidCourse ? 'left' : 'right' }} />
<GenericButton outlined icon="pi pi-external-link" onClick={() => window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip={isMobileView ? null : "View Nostr Event"} tooltipOptions={{ position: paidCourse ? 'left' : 'right' }} />
</div>
) : (
<div className='flex space-x-2 mt-4 sm:mt-0'>
<GenericButton className='my-2' outlined icon="pi pi-external-link" onClick={() => window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip={ isMobileView ? null : "View Nostr Event" } tooltipOptions={{ position: paidCourse ? 'left' : 'right' }} />
<GenericButton className='my-2' outlined icon="pi pi-external-link" onClick={() => window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip={isMobileView ? null : "View Nostr Event"} tooltipOptions={{ position: paidCourse ? 'left' : 'right' }} />
</div>
)}
</div>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef, useCallback } from "react";
import { Tag } from "primereact/tag";
import Image from "next/image";
import ZapDisplay from "@/components/zaps/ZapDisplay";
@ -11,6 +11,7 @@ import dynamic from "next/dynamic";
import { Divider } from "primereact/divider";
import appConfig from "@/config/appConfig";
import useWindowWidth from "@/hooks/useWindowWidth";
import useTrackVideoLesson from '@/hooks/tracking/useTrackVideoLesson';
const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"),
@ -19,13 +20,43 @@ const MDDisplay = dynamic(
}
);
const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }) => {
const [zapAmount, setZapAmount] = useState(0);
const [nAddress, setNAddress] = useState(null);
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" });
const { returnImageProxy } = useImageProxy();
const windowWidth = useWindowWidth();
const isMobileView = windowWidth <= 768;
const [videoDuration, setVideoDuration] = useState(null);
const [videoPlayed, setVideoPlayed] = useState(false);
const mdDisplayRef = useRef(null);
const { isCompleted, isTracking } = useTrackVideoLesson({
lessonId: lesson?.d,
videoDuration,
courseId: course?.d,
videoPlayed
});
const checkDuration = useCallback(() => {
const videoElement = mdDisplayRef.current?.querySelector('video');
if (videoElement && videoElement.readyState >= 1) {
setVideoDuration(Math.round(videoElement.duration));
// Add event listener for play event
videoElement.addEventListener('play', () => {
setVideoPlayed(true);
});
} else if (videoElement) {
setTimeout(checkDuration, 100);
}
}, []);
useEffect(() => {
if (isCompleted) {
setCompleted(lesson.id);
}
}, [isCompleted, lesson.id]); // Remove setCompleted from dependencies
useEffect(() => {
if (!zaps || zapsLoading || zapsError) return;
@ -43,12 +74,19 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
setNAddress(addr);
}, [lesson]);
useEffect(() => {
if (decryptionPerformed && isPaid) {
const timer = setTimeout(checkDuration, 500);
return () => clearTimeout(timer);
}
}, [decryptionPerformed, isPaid, checkDuration]);
const renderContent = () => {
if (isPaid && decryptionPerformed) {
return (
<>
<div ref={mdDisplayRef}>
<MDDisplay className='p-0 rounded-lg w-full' source={lesson.content} />
</>
</div>
);
} else if (isPaid && !decryptionPerformed) {
return (

View File

@ -0,0 +1,52 @@
import prisma from "@/db/prisma";
export const getUserLessons = async (userId) => {
return await prisma.userLesson.findMany({
where: { userId },
include: { lesson: true },
});
};
export const getUserLesson = async (userId, lessonId) => {
return await prisma.userLesson.findUnique({
where: {
userId_lessonId: {
userId,
lessonId,
},
},
include: { lesson: true },
});
};
export const createOrUpdateUserLesson = async (userId, lessonId, data) => {
console.log(`Creating or updating user lesson for user ${userId} and lesson ${lessonId} with data:`, data);
return await prisma.userLesson.upsert({
where: {
userId_lessonId: {
userId,
lessonId,
},
},
update: {
...data,
updatedAt: new Date(),
},
create: {
userId,
lessonId,
...data,
},
});
};
export const deleteUserLesson = async (userId, lessonId) => {
return await prisma.userLesson.delete({
where: {
userId_lessonId: {
userId,
lessonId,
},
},
});
};

View File

@ -0,0 +1,102 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useSession } from 'next-auth/react';
import axios from 'axios';
const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) => {
const [isCompleted, setIsCompleted] = useState(false);
const [timeSpent, setTimeSpent] = useState(0);
const [isTracking, setIsTracking] = useState(false);
const timerRef = useRef(null);
const { data: session } = useSession();
const completedRef = useRef(false);
// Check if the lesson is already completed or create a new UserLesson record
const checkOrCreateUserLesson = useCallback(async () => {
if (!session?.user) return false;
try {
const response = await axios.get(`/api/users/${session.user.id}/lessons/${lessonId}?courseId=${courseId}`);
if (response.status === 200 && response?.data) {
// Case 1: UserLesson record exists
if (response?.data?.completed) {
// Lesson is already completed
setIsCompleted(true);
completedRef.current = true;
return true;
} else {
// Lesson exists but is not completed
return false;
}
} else if (response.status === 204) {
// Case 2: UserLesson record doesn't exist, create a new one
await axios.post(`/api/users/${session.user.id}/lessons?courseId=${courseId}`, {
// currently the only id we get is the resource id which associates to the lesson
resourceId: lessonId,
opened: true,
openedAt: new Date().toISOString(),
});
return false;
} else {
console.error('Error checking or creating UserLesson:', response.statusText);
return false;
}
} catch (error) {
console.error('Error checking or creating UserLesson:', error);
return false;
}
}, [session, lessonId]);
const markLessonAsCompleted = useCallback(async () => {
if (!session?.user || completedRef.current) return;
completedRef.current = true;
try {
const response = await axios.put(`/api/users/${session.user.id}/lessons/${lessonId}?courseId=${courseId}`, {
completed: true,
completedAt: new Date().toISOString(),
});
if (response.status === 200) {
setIsCompleted(true);
setIsTracking(false);
} else {
console.error('Failed to mark lesson as completed:', response.statusText);
}
} catch (error) {
console.error('Error marking lesson as completed:', error);
}
}, [lessonId, session]);
useEffect(() => {
const initializeTracking = async () => {
const alreadyCompleted = await checkOrCreateUserLesson();
// Case 3: Start tracking if the lesson is not completed, video duration is available, and video has been played
if (!alreadyCompleted && videoDuration && !completedRef.current && videoPlayed) {
console.log(`Tracking started for lesson ${lessonId}, video duration: ${videoDuration} seconds, video played: ${videoPlayed}`);
setIsTracking(true);
timerRef.current = setInterval(() => {
setTimeSpent(prevTime => prevTime + 1);
}, 1000);
}
};
initializeTracking();
// Cleanup function to clear the interval when the component unmounts
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed]);
useEffect(() => {
// Case 4: Mark lesson as completed when 90% of the video is watched
if (videoDuration && timeSpent >= Math.round(videoDuration * 0.9) && !completedRef.current) {
markLessonAsCompleted();
}
}, [timeSpent, videoDuration, markLessonAsCompleted]);
return { isCompleted, isTracking };
};
export default useTrackVideoLesson;

75
src/lib/github.js Normal file
View File

@ -0,0 +1,75 @@
const { Octokit } = require("@octokit/rest");
const { throttling } = require("@octokit/plugin-throttling");
const ThrottledOctokit = Octokit.plugin(throttling);
const ACCESS_TOKEN = process.env.NEXT_PUBLIC_GITHUB_ACCESS_KEY;
const octokit = new ThrottledOctokit({
auth: ACCESS_TOKEN,
throttle: {
onRateLimit: (retryAfter, options, octokit, retryCount) => {
octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
if (retryCount < 2) {
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
return true;
}
},
onSecondaryRateLimit: (retryAfter, options, octokit) => {
octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`);
return true;
},
},
});
async function getContributions(username, updateCallback) {
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
const sinceDate = sixMonthsAgo.toISOString();
const contributionData = {};
try {
const repos = await octokit.paginate(octokit.repos.listForUser, {
username,
per_page: 100,
});
console.log(`Fetched ${repos.length} repositories for ${username}`);
// Call updateCallback immediately after fetching repos
updateCallback({});
for (const repo of repos) {
console.log(`Fetching commits for ${repo.name}`);
try {
const commits = await octokit.paginate(octokit.repos.listCommits, {
owner: repo.owner.login,
repo: repo.name,
author: username,
since: sinceDate,
per_page: 100,
});
console.log(`Fetched ${commits.length} commits for ${repo.name}`);
commits.forEach(commit => {
const date = commit.commit.author.date.split('T')[0];
contributionData[date] = (contributionData[date] || 0) + 1;
// Call the update callback after processing each commit
updateCallback({...contributionData});
});
} catch (repoError) {
console.error(`Error fetching commits for ${repo.name}:`, repoError.message);
}
}
console.log('Final contribution data:', contributionData);
return contributionData;
} catch (error) {
console.error("Error fetching contribution data:", error);
throw error;
}
}
export { getContributions };

View File

@ -0,0 +1,54 @@
import { getUserLesson, createOrUpdateUserLesson, deleteUserLesson } from "@/db/models/userLessonModels";
import { getResourceById } from "@/db/models/resourceModels";
// todo somehow make it to where we can get lesson slug in this endpoint
export default async function handler(req, res) {
const { method } = req;
const { slug, resourceSlug, courseId } = req.query;
switch (method) {
case "GET":
try {
const resource = await getResourceById(resourceSlug);
const lesson = resource?.lessons.find((lesson) => lesson.courseId === courseId);
const lessonId = lesson?.id;
const userLesson = await getUserLesson(slug, lessonId);
if (userLesson) {
res.status(200).json(userLesson);
} else {
res.status(204).end();
}
} catch (error) {
res.status(500).json({ error: error.message });
}
break;
case "PUT":
try {
const data = req.body;
const resource = await getResourceById(resourceSlug);
const lesson = resource?.lessons.find((lesson) => lesson.courseId === courseId);
const lessonId = lesson?.id;
const updatedUserLesson = await createOrUpdateUserLesson(slug, lessonId, data);
res.status(200).json(updatedUserLesson);
} catch (error) {
res.status(400).json({ error: error.message });
}
break;
case "DELETE":
try {
const resource = await getResourceById(resourceSlug);
const lesson = resource?.lessons.find((lesson) => lesson.courseId === courseId);
const lessonId = lesson?.id;
await deleteUserLesson(slug, lessonId);
res.status(204).end();
} catch (error) {
res.status(500).json({ error: error.message });
}
break;
default:
res.setHeader("Allow", ["GET", "PUT", "DELETE"]);
res.status(405).end(`Method ${method} Not Allowed`);
}
}

View File

@ -0,0 +1,36 @@
import { getUserLessons, createOrUpdateUserLesson } from "@/db/models/userLessonModels";
import { getResourceById } from "@/db/models/resourceModels";
// todo somehow make it to where we can get lesson slug in this endpoint
export default async function handler(req, res) {
const { method } = req;
const { slug, courseId } = req.query;
const userId = slug;
switch (method) {
case "GET":
try {
const userLessons = await getUserLessons(userId);
res.status(200).json(userLessons);
} catch (error) {
res.status(500).json({ error: error.message });
}
break;
case "POST":
try {
const { resourceId, ...data } = req.body;
const resource = await getResourceById(resourceId);
const lesson = resource?.lessons.find((lesson) => lesson.courseId === courseId);
const lessonId = lesson?.id;
const userLesson = await createOrUpdateUserLesson(userId, lessonId, data);
res.status(201).json(userLesson);
} catch (error) {
res.status(400).json({ error: error.message });
}
break;
default:
res.setHeader("Allow", ["GET", "POST"]);
res.status(405).end(`Method ${method} Not Allowed`);
}
}

View File

@ -10,6 +10,7 @@ import { useSession } from 'next-auth/react';
import { nip04, nip19 } from 'nostr-tools';
import { ProgressSpinner } from 'primereact/progressspinner';
import { Accordion, AccordionTab } from 'primereact/accordion';
import { Tag } from 'primereact/tag';
import { useDecryptContent } from "@/hooks/encryption/useDecryptContent";
import dynamic from 'next/dynamic';
@ -58,12 +59,9 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
const [lessons, setLessons] = useState([]);
const [uniqueLessons, setUniqueLessons] = useState([]);
console.log('lessonIds', lessonIds);
useEffect(() => {
if (lessonIds.length > 0) {
const fetchLesson = async (lessonId) => {
console.log('lessonId', lessonId);
try {
await ndk.connect();
const filter = { "#d": [lessonId], kinds:[30023, 30402], authors: [pubkey] };
@ -143,6 +141,12 @@ const Course = () => {
const { showToast } = useToast();
const [paidCourse, setPaidCourse] = useState(false);
const [expandedIndex, setExpandedIndex] = useState(null);
const [completedLessons, setCompletedLessons] = useState([]);
const setCompleted = (lessonId) => {
console.log('setting completed', lessonId);
setCompletedLessons(prev => [...prev, lessonId]);
}
const fetchAuthor = useCallback(async (pubkey) => {
const author = await ndk.getUser({ pubkey });
@ -155,6 +159,10 @@ const Course = () => {
const { lessons, uniqueLessons, setLessons } = useLessons(ndk, fetchAuthor, lessonIds, course?.pubkey);
const { decryptionPerformed, loading } = useDecryption(session, paidCourse, course, lessons, setLessons);
useEffect(() => {
console.log('lessonIds', lessonIds);
}, [lessonIds]);
useEffect(() => {
if (course?.price && course?.price > 0) {
setPaidCourse(true);
@ -230,14 +238,15 @@ const Course = () => {
accordiontab: { className: 'border-none' },
}}
header={
<div className="flex align-items-center justify-content-between w-full">
<div className="flex align-items-center justify-between w-full">
<span id={`lesson-${index}`} className="font-bold text-xl">{`Lesson ${index + 1}: ${lesson.title}`}</span>
{completedLessons.includes(lesson.id) ? <Tag severity="success" value="Completed" /> : null}
</div>
}
>
<div className="w-full py-4 rounded-b-lg">
{lesson.type === 'video' ?
<VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} /> :
<VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} /> :
<DocumentLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} />
}
</div>

13
src/pages/testr.js Normal file
View File

@ -0,0 +1,13 @@
import React from 'react';
import GithubContributionChart from '@/components/charts/GithubContributionChart';
const Testr = () => {
return (
<div className="container mx-auto p-4">
<GithubContributionChart username="austinkelsay" />
</div>
);
};
export default Testr;