From e018b3ad54d334a69d6af2dcc25bca7a8d1d3a12 Mon Sep 17 00:00:00 2001 From: Ayush Date: Sat, 7 Mar 2026 02:22:39 +0530 Subject: [PATCH 1/4] refactor: Update references from EthEd to EIPsInsight Academy across the codebase - Updated course descriptions and email templates to reflect the new branding. - Changed subdomain validation from "ethed" to "eipsinsight". - Modified IPFS handling to use new identifiers and image URIs. - Enhanced NFT metadata generation to include unique SVG certificates. - Adjusted database connection URL examples for clarity. - Updated proxy security comments to align with new platform branding. --- .github/workflows/backup.yml | 2 +- README.md | 10 +- SETUP.md | 4 +- _find-region.mjs | 46 ++ package.json | 2 +- public/logos/logo-old.png | Bin 0 -> 2936 bytes public/logos/logo.png | Bin 4082 -> 424 bytes public/manifest.json | 4 +- scripts/generate-apple-secret.js | 4 +- scripts/pin-genesis-assets.mjs | 2 +- src/__tests__/genesis-assets.test.ts | 2 +- src/app/(public)/_components/features.tsx | 4 +- src/app/(public)/_components/footer.tsx | 8 +- src/app/(public)/_components/navbar.tsx | 39 +- src/app/(public)/courses/eips-101/page.tsx | 8 +- src/app/(public)/page.tsx | 18 +- src/app/(public)/privacy/page.tsx | 2 +- src/app/(public)/terms/page.tsx | 12 +- src/app/403/page.tsx | 2 +- src/app/about/page.tsx | 12 +- src/app/admin/nfts/page.tsx | 2 +- src/app/api/admin/reviews/route.ts | 4 +- src/app/api/courses/route.ts | 2 +- .../instructor/courses/[courseId]/route.ts | 1 - .../courses/[courseId]/submit/route.ts | 2 +- src/app/api/og/profile/[id]/route.tsx | 4 +- src/app/api/payments/siwe/challenge/route.ts | 2 +- src/app/api/user/nft/mint/route.ts | 24 +- src/app/api/user/nft/share/route.ts | 4 +- src/app/api/user/wallets/route.ts | 4 +- src/app/community/layout.tsx | 8 +- src/app/community/page.tsx | 15 +- src/app/dashboard/layout.tsx | 2 +- src/app/dashboard/page.tsx | 4 +- src/app/donate/layout.tsx | 8 +- src/app/donate/page.tsx | 2 +- src/app/how-it-works/layout.tsx | 8 +- src/app/how-it-works/page.tsx | 4 +- src/app/layout.tsx | 22 +- src/app/leaderboard/layout.tsx | 8 +- src/app/leaderboard/page.tsx | 2 +- src/app/learn/layout.tsx | 6 +- src/app/nft/[id]/page.tsx | 10 +- src/app/not-found.tsx | 2 +- src/app/onboarding/_components/Onboarding.tsx | 2 +- src/app/pricing/layout.tsx | 8 +- src/app/pricing/page.tsx | 2 +- src/app/profile/[id]/page.tsx | 4 +- .../profile/_components/ProfilePortfolio.tsx | 43 +- src/app/projects/layout.tsx | 8 +- src/app/robots.ts | 2 +- src/app/settings/layout.tsx | 4 +- src/app/sitemap.ts | 2 +- src/components/CourseModulePage.tsx | 77 ++- src/components/EnhancedLessonViewer.tsx | 68 ++- src/components/LessonViewer.tsx | 6 +- src/components/forms/CourseCreationForm.tsx | 2 +- src/components/forms/ProfileSetupForm.tsx | 2 +- src/components/logo.tsx | 47 +- src/components/nft-share-modal.tsx | 12 +- src/components/seo/JsonLd.tsx | 18 +- src/components/sidebar/site-header.tsx | 2 +- src/components/siwe-login-button.tsx | 2 +- src/lib/ai-client.ts | 2 +- src/lib/auth.ts | 6 +- src/lib/certificate-generator.ts | 446 ++++++++++++++++++ src/lib/courseData.ts | 2 +- src/lib/emails/courseCompletion.ts | 6 +- src/lib/ens-service.ts | 2 +- src/lib/ipfs.ts | 2 +- src/lib/lessonContentMap.ts | 2 +- src/lib/metrics.ts | 2 +- src/lib/nft-service.ts | 296 ++++++++---- src/lib/prisma-client.ts | 41 +- src/lib/viem-client.ts | 2 +- src/proxy.ts | 2 +- 76 files changed, 1126 insertions(+), 327 deletions(-) create mode 100644 _find-region.mjs create mode 100644 public/logos/logo-old.png create mode 100644 src/lib/certificate-generator.ts diff --git a/.github/workflows/backup.yml b/.github/workflows/backup.yml index d6f9ce4..368d537 100644 --- a/.github/workflows/backup.yml +++ b/.github/workflows/backup.yml @@ -20,7 +20,7 @@ jobs: DATABASE_URL: ${{ secrets.DATABASE_URL }} run: | TIMESTAMP=$(date +%Y%m%d_%H%M%S) - FILENAME="ethed_backup_${TIMESTAMP}.sql.gz" + FILENAME="eipsinsight_backup_${TIMESTAMP}.sql.gz" pg_dump "$DATABASE_URL" | gzip > "$FILENAME" echo "BACKUP_FILE=$FILENAME" >> $GITHUB_ENV diff --git a/README.md b/README.md index a0ceac4..06d419a 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# EthEd Frontend +# EIPsInsight Academy Frontend **Blockchain education made interactive, verifiable, and rewarding.** -EthEd transforms Web3 learning with NFT achievements, AI tutoring, and gamified progress tracking. +EIPsInsight Academy transforms Web3 learning with NFT achievements, AI tutoring, and gamified progress tracking. ## 🚀 What We've Built ### Interactive Experience - **Global Grid System**: Full-viewport canvas with mouse-tracking glow effects inspired by Linear/Stripe/Vercel - **Smart Content Detection**: Grid brightness adapts automatically over text for perfect readability -- **EthEd Agent**: Bottom-right hover assistant with smooth animation cycles (p1→pause→p3→pause2) +- **EIPsInsight Agent**: Bottom-right hover assistant with smooth animation cycles (p1→pause→p3→pause2) - **Dialog Persistence**: Agent dialog stays open when clicking inside, closes when clicking outside ### Authentication & Infrastructure @@ -34,8 +34,8 @@ EthEd transforms Web3 learning with NFT achievements, AI tutoring, and gamified 1. **Clone and install** ```bash - git clone https://github.com/AyuShetty/ethed-frontend.git - cd ethed-frontend + git clone https://github.com/AyuShetty/eipsinsight-academy-frontend.git + cd eipsinsight-academy-frontend pnpm install ``` diff --git a/SETUP.md b/SETUP.md index b2af2da..3699675 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,4 +1,4 @@ -# EthEd Setup Guide +# EIPsInsight Academy Setup Guide ## Environment Configuration @@ -61,7 +61,7 @@ ## Database Setup 1. **Install PostgreSQL** (if not already installed) -2. **Create a database** named `ethed` +2. **Create a database** named `eipsinsight` 3. **Update DATABASE_URL** in `.env.local` 4. **Run migrations:** ```bash diff --git a/_find-region.mjs b/_find-region.mjs new file mode 100644 index 0000000..7b0047d --- /dev/null +++ b/_find-region.mjs @@ -0,0 +1,46 @@ +import net from "net"; + +const user = "postgres.swsveygecsdalkkcufbx"; +const db = "postgres"; +const regions = [ + "ap-south-1","ap-southeast-1","ap-southeast-2","ap-northeast-1", + "us-east-1","us-west-1","eu-west-1","eu-central-1","ca-central-1","sa-east-1" +]; + +function probe(region) { + return new Promise((resolve) => { + const host = `aws-0-${region}.pooler.supabase.com`; + const c = net.createConnection(6543, host, () => { + const params = `user\x00${user}\x00database\x00${db}\x00\x00`; + const len = 4 + 4 + Buffer.byteLength(params); + const buf = Buffer.alloc(len); + buf.writeInt32BE(len, 0); + buf.writeInt32BE(196608, 4); // protocol 3.0 + buf.write(params, 8, "utf8"); + c.write(buf); + }); + let data = Buffer.alloc(0); + c.on("data", (d) => { data = Buffer.concat([data, d]); c.destroy(); }); + c.on("close", () => { + const str = data.toString("utf8"); + if (str.includes("Tenant or user not found")) { + resolve({ region, status: "WRONG_REGION" }); + } else if (str.includes("FATAL") || str.includes("password") || str.includes("auth")) { + resolve({ region, status: "FOUND - needs auth", raw: str.slice(0,120) }); + } else if (data.length > 0) { + resolve({ region, status: "RESPONDED", raw: data.toString("hex").slice(0,80) }); + } else { + resolve({ region, status: "NO_DATA" }); + } + }); + c.on("error", (e) => resolve({ region, status: "ERR: " + e.message })); + setTimeout(() => { c.destroy(); resolve({ region, status: "TIMEOUT" }); }, 4000); + }); +} + +const results = await Promise.all(regions.map(probe)); +for (const r of results) { + const icon = r.status.startsWith("FOUND") ? "✅" : r.status === "WRONG_REGION" ? "❌" : "⚠️"; + console.log(`${icon} ${r.region.padEnd(18)} ${r.status}`); + if (r.raw) console.log(` └─ ${r.raw.replace(/\n/g," ")}`); +} diff --git a/package.json b/package.json index 2aa85f4..24e9161 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "ethed-frontend", + "name": "eipsinsight-academy-frontend", "version": "0.1.0", "private": true, "packageManager": "pnpm@9.15.4", diff --git a/public/logos/logo-old.png b/public/logos/logo-old.png new file mode 100644 index 0000000000000000000000000000000000000000..cfc64a39d0b52fa53bbdabc2240e72869293a4a3 GIT binary patch literal 2936 zcmZ`*c{G%78-6hu290g(%TSVRAtX!2I(FG*D`iRcoiJIl%;0A!Wk@L5P02Qh(q&W`1S>09et6y4L}K2HXPI zptRr`rKPzF05C1IuC{eV0lCoI#LATC%QC9L(D$OvHEUaTrRz1?@C6QTZf^KT*2^jO zobvikCD~q1C6okZOAg@@o6_gao9{743~5m2M$FfF{XlK58m`-`Nn&KYe0P5g%*&xC zY;eQss$%%S>dUyLzNal#s8_>Cz)eb`oeTg}2~hwugaH73I$D626$-d1{m(%mduN#` z9-83o<@H?F#MrniDVa@3fuMuH-nnz9A%ZerWmgw=XNagVtUo_eWjF2=6cof0ZHPv% z`fXrN6ls-IR4zX1IojK(b8Zcz%zrwI-fc99;nC94iWhBbYm1s>{|tTJ*l4>a*Vos_ zvO%b>#>M&u2XEK8cKn&?(DE6TP9!BTo)Hrlw~V`XmSZjx&dWPK`3^V^gUDaH#HV?` zIWu#x)F4yBK>9C+61x3Y$?E|qauhWy`k&zLhe6Pp3x#iZCF-N;CEiHknf|^BI zp=;ltJmIaeojjD%k#1CB8ok@#IrywlGe#}*OFeMCochqRCSQdb^ipW!K59O@=!Kxw*RB z4D|HKL=#ifGlHrXj*b%~l7)o@Tu{*DSmv>u>ok#(lao_8lSfF%*~Mj=L=w&Gtxz{N zGh;heJUl!UO9Ux6TCBy~wK`0@zr8$|C+Et+$*CvC?nm&S(X8Ck=zxmp8!Z)VpDnasvez@gBGrJE#atR1jh2KEw=$wiH%J@P&-q`>2 zS1(VzC>#ay^i^I0pr)O-1USj=ZB)6PJ9kcKjWjxH>(AXcFyObdHd5!(j)EQ>9f_z% z{BsxAQCe13COvve9|Yr(6})r{!Y0h}R)B*;A$Z9oDiw=`u^DqQ>owWu8oh)f4Gg}d zq(Iv%7rtb!hZ=hR>Us1%An|oWLufz%X`7YzNse0q)6VX01{P~~GFNi+g&E=rjdM(Xg8j&2=X6bh8bF+}vCc1uHA7DEHU)xYIy=1B1!Q zN&hM0&QPgA)QmCRd+j#iaX}6C@7`FgxRa0GVs>_8$^tl{tGh+3b))Z&54M+!${w6( z(Ege0QvTIcQzR7V|B@ii8#S497t;`tDJxSk&ipM@9*zVXGa&+ISq{0 z1lW&|Yia3Rf&Er)F0K$zPBvybczAe<)FU}w;e@qf1-ZFL45a5TD4$6=y_{z6e`|@ z8~bUCLK!lir4sepWRx^CoXzR@(>;!M$y4)47eP$v2`>_Z)OiR^LM4(OU9@u?(~LV2 zL@q8ZjfPAA8GlW;ESnG?FYDC236g4JV&c5{vxw+uPY;iiJ3^fS0nc|%j-%~GNobY{ev)^IshReOn?ijg&t_|?_bv$Jk(zlpw+jqlt8sVhS&em@Cs-&WVw zPB#ZG5N>uws86>=ZYwA#{5?MAk+#S1Xb|WBJqh^S{o@Cpau@3+JK*_?BE0(XqprER zIa{(~$O>t7_|`nIJ{2I&Oh!c7Y*)hZlDaLLT4WSYLmm;jWF5ki?*8;(@n(jmf+-+EZY zPIh=7J%prF?byJKw6?Kn0GUChQY$N^vd_YpnL!6QBch4hQ(jPFC*@p&eh>crquxL) z>#f5%1oq|iifKQs=ZpXiRT32J>lIXxoS;FOnwp9mWKg%ZCI~m_%o1od&ird~@{Ip= zyFxCEu2^bX+M3b6wS$BA3vv(O=0sy{Yb*802!Y+1>q<6rVgq852kxQ3;osfdbbDic6^=ltAUu2VYS5ygg9G26n#M+DFhoUX$6vdH8w!Qe z4BGL}i3vxsEEawxNlnez;WAT8OG{8bYier|%t>H0J6Ku{y+G#>Hy#3gC0(@pe-G*_ zDk^Gf$WJc0H#_+S23~rcXm~a#B;;}mu`QBSK3yMWO&Viz6~wD zXt4+=Ghm8}3hxzCG1zPBV>y>Lc@>p^XE&A&E<*mi zyj@XWSfNuGq5do{&kp3r@fMM&_7~$CZIZ9VWT5@MA{B6>!pobRpLEU5`fF)kP5-j+ zYk9e~wYB%x)|)p9*=NlQ)k2RCLlM}K@o~fK#U}q5P~Coh|Ndfl_2<;o9R&w(Z*OO3 zp-|hU+zVFBFc>#;l74CAA@zc0>vqvIXwrug1T}*d0*Ge-4h{}cG))$gx!t!x9N3`H zl=Sp-=x_b~{bOT!qA6o_t{cuZzM-=EjTw1DQS>G$u1MLOP4vPurBGC65Njwg$}sRtLEnYzNdDT0o&#^9eQ23phVV#hUg7bz3o`XzAEZipq zNa6Hvd;j}S>TR>lX~mnvxBSca-4rPs|Y@Pp!FGKYQ=T%W2ca(_u44G_`eVL(n?I~F%Z`PQI z!AJ{*ELp~uWi(1i{CxlWy?@;MoadZ-?mg!@_nv#6WJ?Rn2;^M9Gel^@}fX|GDHr3 z1AUz0a}6{^U+KVvt-!mc}lCCm1I!~2?=yZ)NT$>#$fT)*rAb;k-fmCwp$NRI070L78V}Mazh>3`dhet;4#8iwx7M}}lS>}1&N^J#IjGV^bmz9-G&lHl+${2Lw5W$bC!oGV7 zQ}uwas(5!5w&gwmTnIojOu4v#W(S4y`vO#V>+v8}AQZI5G1IA9Z@>wv;n*yHVW5&Z z4D2Hn0(efHav2V6x(fNFmI>-efb=PPGWwoFX{`(m93}B4YAB-Lfzdylz;rCCIMrfj zRKwO0keq1e_~MHXi&0wLhWNZDC7}L83jf*Kq_?80d)<0=R<3$;)3}-hSyL2H4}M^4 zOBE~cLNrahHvXPcDbH~LR6k9QH6y(FcvO1RdRDSh3TLTMdE!Qc&ZLZhKz*C7mL&iw zr(g@LXE2T}sz1DvNDW(t*0UEdOrw4PY_k@9xo6|-fCjK=;&sE^_HoP>Fqmly=BBnP z_@BEWziEs_+}OXaz;l-uN>Ult;_niE6emgE^Z}-*T9=cZD@q|np!$}%pWuKgH9)bP z!gDmJ@YBMdLfecM|7sR@WlAGkr;&p0q^!Vj!VJtmSq@OFps=0F=jMfVQ;U2I;)>bb zbgFAkIOVI95%d$F9w{d?@5Tumx@9NfdMSO+6fEOO-gS(>8YNWC*YqkW8WX0nfjkn$ z`$&{?Pyux-8V_x?yvy+ERFz7B2eB4pkrYc1Nj(NqWT&9FXJL_3Dl2O}f2*{sjQ1^z1B>q;MNXwwN z-~D~X0S_Lk<;+XJwwq)h;FwXRzdeA0<_hxT6G6HI3DB(b05sg3{x8t}maTxc1-!~N z1&iPjQF*%j_*k|vSI*0ii7!=3oL4mPu7GeZDEgC!*R58e!Wk6MnQO{*l`AOAb_lL2 z2+^C$k(CrMyxJ#zlhe)EOc#9?Q}5)V{Y_@HFWW@W%7=E&iC+j+)FT7)83juUyn;wS zVSU<4&GQcH*Jd#&86xezuhN%UAnINc27?U_Mi_O*C~;E*S#G?Y4A(8u%H=;7JmlCv zn!X}l9uuYh0BYwl7$-y}iZORE>i7jfnY~`3xXNz!z6yQ&BUr_ZKs6sd4?O0Waabb+ zFyfycerUd{S0yHp-lPRzTPa$k=X;D`9A$_krhE_O*?q2Xv0t9WND;ooy~njYBR#*E^$xv%Y<>#+ z1(g=j-<~B(>HTqw-aS$76q3)pMAxIA9(Fj?HfzpNc7jZ82{R&H62qZO&2M!N?h_vj zmLpX8MLM57sS7J$quy@`GKCG57@Gf$;scC`E?vmC>Ke(B@31i#6CfWgb5l8E3;J8p zFf2c|R%Q_53~2s%d}jM}>lt!CB;epLq|V8Ro)s)7&0Np2NJtGnvmDLjAcw(5DI4e7R#J7imacgzC1h6egYwl~VHu3t& z!XsdC@ay%8vF0Rl$5T^ZalD^T4*4ne#zD!?QCHu_dgWH2s8n_?z^KA@M8^i$;0#LQ zkxQxr?UdVIK_Q;sed=l;E)p%&A^Gz`&6)+*rs07rquvPAOd9sD4HwD!P1s0Mdy>4) zA_%{`mpM-~|6mrOY`?g*d#5phTsj|edFqZGBT@_glcaw;Px@h-((gu-TPp_%N$G*8 zA-CoYD}H(BZTm%37n)L)v_xDuM(=h|+8U2HjTTY$(fw1F7i9BwQ0vn6$7~wEjMutR znI8M<+2M%m4p?JY4Y8}%3(Wkt=PabcY{J}&|Fhl(;aTrH<)h=9StNUv0lGtnYJiT7 zcNzW5vNJGL>0V6~M9qKgu9H~2>^2M@j&R3^t~R}FLM`Snj^BSI+3_dW|DLy=(u_usD_T!kakGyZNVxR$xzHX6VfzFVECW8XY8bhy!5@U5J$K{{N09dwVVOzt@e z{lXr}hyO-rKlTzpdjz+PkO#M?a}0|UHKYamjX`LoT!GRtwNF#4sl@8h_RF6LEj{7l zqaVSWxO*Dn7T$%3wCVV{i~!yPrkZK22z9AE1fuFI?eENr5s`n>kr#?)*&O{171WTX z7x+cr0O{LjBf18@5ELa$UDf^@niTxbg_!OYwi4^sax<5b_5<5sWfk|(HF^+p?(S}+ zY;}Ig7%5BoFB4CDu8zUgckz2uU;ZG+Udur|(u`*`Gky@)w08Ik7XtON2gp zS7N)IJ!-?Qf~5D-l}l7F{E8o!ifik#{hxQbvgJV&p0dGHB6RZqdflcYPsDg=(_~;4 zkmpKEZEzu%*FAafwWU|zAa-E5%=pOukUE70gILfPV(-_fWy`@8k?)V z*90=KUiw-t#l@&E{%#9TW&peEWDtDugVK6C-GeXB; z>UHVkKV3U)I5UWaF1kwWP&t4X9;Lm+pxWCtbddvgd=e>;E=2fa8$2Q;=B?&tk!Qur zGM?p3;+I|e#Rz`K9(SAGZ!jWT|H4L+x11HqD~|3$NKxR7IDrBQI^0|wz4HGhSEVgB~1 z5`T|7|M0Y0yPtmPWHK1D1RF9M{cS1iZM=5kcYFh+6X{`pt+(76dMB5gw=+BC8U;Tt zF61R*{@T`g#_p|mMb2!RqdF|G6Xv>)mKG5AP=oh2_#tIVOjSisB8FC%#;x}xZ(9v; zRv(9hcA9NDH`(ys6%_7g%9}}8J+=2*ZT<#+0RlgI-nYc*e2rgy_G`pm4mS0+raXL1 z2f>iHD1^Dn^;fw~_v1LP#XlHS_n>!~V^oeNF1&l`M3@ z-uu;X>V>(Lb;On1l36ZVXS0SH8l@tt^;{G}K0F)|+tqpX>DhD1=j~Kp1S(^8OtwP~ zt>Odik~Cl%3x@a)E+U0_wIowC-Xp15^RE#+4aN+?KxD#RC?51I8!>OO{zmCLdk(WP>Frd6L0e<9X`g#uQ*nLi|{D`ysxV~pfnHN1ea8pPgnYoDth< zUV$I?Qwsgo9Y>Fw)z%2(@Em7uTZ>-0nF~5~1-h&__BukuoVS$zYooM2;^IBJm))yJ z4ArtWyQ-b`d3~(*w5j1_Zb^C77*z*0(uc zGL=!9L>5!xb5JBu3F;-wEgK(vI-pgmtRF_zn{IxyG`9PKO{N@bA;dTJ{tzvrY0|u# z*TuV}LQpfousoD~!`tl7z*7n}oimMjwE4@gEs{-&)T3(B`Q7#J1%M3x?!IA`nHW@= zISVe5C*QWEsmIkUzb2!(i`{S zHe7z?2vHKTKJ=woYNZmoIUIUwIU z=4?q_8cFpvXdMtO4tPVGsOWkSbwGS^|9EZ5kWP6iBrJa%8uc|p<4vdLkGhn;^u1={ zH|j+1*oB3u-e6qwr#e-ZZ-oJ>C1jy*t|#?Q^5!wcR01%>{diBc4KEuS{jUqx3|&rX zL;OB>Y;j-o;8q8F2R`k}9{43JDlwSu*Yc`tqIMGFb~R@5ljdVopEm^uC|3wovK@o*gsbvTM5$$zw(53l&Kh=Xhs!W>Lc$tU&wzP?tlI6 ee;Db_KCiBN1V+nj&G^Wt111Oyc&&k3{Qm)JNU2)@ diff --git a/public/manifest.json b/public/manifest.json index e554b6c..df16c8d 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "name": "EthEd - Blockchain Education", - "short_name": "EthEd", + "name": "EIPsInsight Academy - Blockchain Education", + "short_name": "EIPsInsight", "description": "Blockchain education made interactive, verifiable, and rewarding.", "start_url": "/", "display": "standalone", diff --git a/scripts/generate-apple-secret.js b/scripts/generate-apple-secret.js index 4c91fa0..0140346 100644 --- a/scripts/generate-apple-secret.js +++ b/scripts/generate-apple-secret.js @@ -4,7 +4,7 @@ const path = require('path'); // Configuration - Replace these with your actual values const TEAM_ID = '2R2CQNX632'; // Found in Apple Developer account membership -const CLIENT_ID = 'com.ethed.webapp.signin'; +const CLIENT_ID = 'com.eipsinsight.webapp.signin'; const KEY_ID = 'M7N46GNRXN'; const PRIVATE_KEY_FILE = 'AuthKey_M7N46GNRXN.p8'; @@ -28,7 +28,7 @@ try { console.error('❌ Error: Please update the configuration in this script!'); console.error('\nYou need to replace:'); console.error('- TEAM_ID: Your Apple Team ID'); - console.error('- CLIENT_ID: Your Services ID (e.g., com.ethed.webapp.signin)'); + console.error('- CLIENT_ID: Your Services ID (e.g., com.eipsinsight.webapp.signin)'); console.error('- KEY_ID: Your Sign In with Apple Key ID'); console.error('- PRIVATE_KEY_FILE: Name of your .p8 file'); process.exit(1); diff --git a/scripts/pin-genesis-assets.mjs b/scripts/pin-genesis-assets.mjs index 39d518b..dc6acee 100644 --- a/scripts/pin-genesis-assets.mjs +++ b/scripts/pin-genesis-assets.mjs @@ -74,7 +74,7 @@ async function main() { { trait_type: "Edition", value: "Pioneer" }, { trait_type: "Rarity", value: "Founder" }, ], - external_url: "https://ethed.app", + external_url: "https://academy.eipsinsight.com", }; console.log("Uploading Genesis metadata template"); diff --git a/src/__tests__/genesis-assets.test.ts b/src/__tests__/genesis-assets.test.ts index 57ffc66..21580f2 100644 --- a/src/__tests__/genesis-assets.test.ts +++ b/src/__tests__/genesis-assets.test.ts @@ -3,7 +3,7 @@ import { GENESIS_PIONEER_IMAGE_URI, GENESIS_PIONEER_METADATA_URI } from '@/lib/g describe('genesis assets', () => { it('requires genesis image to be pinned or PINATA_JWT present (enforced in CI/production)', () => { - const placeholder = 'ipfs://QmEthEdPioneer1' as string; + const placeholder = 'ipfs://QmEIPsInsightPioneer1' as string; const pinned = typeof GENESIS_PIONEER_IMAGE_URI === 'string' && GENESIS_PIONEER_IMAGE_URI !== placeholder; // Only enforce pinning in CI or production builds; local dev uses a bundled fallback image. diff --git a/src/app/(public)/_components/features.tsx b/src/app/(public)/_components/features.tsx index 6378478..a42b75e 100644 --- a/src/app/(public)/_components/features.tsx +++ b/src/app/(public)/_components/features.tsx @@ -75,7 +75,7 @@ const iconPulse = { hover: { scale: 1.12, filter: 'drop-shadow(0 0 16px #22d3ee)', transition: { yoyo: 2, duration: 0.35 } }, }; -export default function EthEdFeatures() { +export default function EIPsFeatures() { return (
@@ -91,7 +91,7 @@ export default function EthEdFeatures() { Built for Web3 Learners

- EthEd combines on-chain rewards, AI support, and ENS identity to help everyone master blockchain—securely and transparently. + EIPsInsight Academy combines on-chain rewards, AI support, and ENS identity to help everyone master blockchain—securely and transparently.

- EthEd + EIPsInsight Academy

Blockchain education made interactive, verifiable, and rewarding. @@ -32,10 +32,10 @@ export default function Footer() { Terms of Service Donate

- + - +
@@ -43,7 +43,7 @@ export default function Footer() {
- © {new Date().getFullYear()} EthEd. Built for the decentralized future. + © {new Date().getFullYear()} EIPsInsight Academy. Built for the decentralized future.
); diff --git a/src/app/(public)/_components/navbar.tsx b/src/app/(public)/_components/navbar.tsx index 92e6c20..238aa11 100644 --- a/src/app/(public)/_components/navbar.tsx +++ b/src/app/(public)/_components/navbar.tsx @@ -103,10 +103,22 @@ export default function Navbar() { {isMobileMenuOpen && (
@@ -429,7 +429,6 @@ export default function Navbar() {
)} -
); } diff --git a/src/app/(public)/courses/eips-101/page.tsx b/src/app/(public)/courses/eips-101/page.tsx index 9bff01b..43e5db8 100644 --- a/src/app/(public)/courses/eips-101/page.tsx +++ b/src/app/(public)/courses/eips-101/page.tsx @@ -76,11 +76,11 @@ const courseModules = [ { id: 8, title: 'Draft Your First EIP', - description: 'Hands-on workshop using EthEd Proposal Builder', + description: 'Hands-on workshop using EIPsInsight Academy Proposal Builder', duration: '45 min', type: 'interactive', completed: false, - content: '/EIPs101.md#9-drafting-a-first-eip-with-etheds-proposal-builder' + content: '/EIPs101.md#9-drafting-a-first-eip-with-eipsinsight-proposal-builder' }, { id: 9, @@ -157,7 +157,7 @@ export default function EIPs101Course() { EIPs 101: From First Principles to First Proposal

- Master Ethereum Improvement Proposals from basics to writing your first EIP using EthEd's tools. + Master Ethereum Improvement Proposals from basics to writing your first EIP using EIPsInsight Academy's tools.

@@ -326,7 +326,7 @@ export default function EIPs101Course() {
- Draft your first EIP using EthEd's Proposal Builder + Draft your first EIP using EIPsInsight Academy's Proposal Builder
diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index e1fa041..aaec67f 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -4,13 +4,13 @@ import React from 'react'; import { ArrowRight, ChevronRight, GraduationCap } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { motion } from 'motion/react'; -import EthEdFeatures from './_components/features'; +import EIPsFeatures from './_components/features'; import Link from 'next/link'; import { Rocket, Globe } from 'lucide-react'; import HowItWorks from './_components/how-it-works'; import Stats from './_components/stats'; -export default function EthEdHero() { +export default function AcademyHero() { return (
{/* Enhanced background effects */} @@ -42,7 +42,7 @@ export default function EthEdHero() { New - EthEd Platform launched! + EIPsInsight Academy launched!
@@ -57,7 +57,7 @@ export default function EthEdHero() { > Get Rewarded for Learning
- On EthEd, your progress is owned by you. + On EIPsInsight Academy, your progress is owned by you. @@ -68,7 +68,7 @@ export default function EthEdHero() { transition={{ duration: 0.5, delay: 0.2 }} className="text-slate-300 mx-auto mt-6 max-w-2xl text-center text-lg leading-relaxed" > - EthEd helps you master blockchain from scratch—guided by AI, powered by real rewards. Earn points, NFT badges, and on-chain certificates as you learn. Sign up instantly (no wallet needed) and claim your unique ENS identity! + EIPsInsight Academy helps you master blockchain from scratch—guided by AI, powered by real rewards. Earn points, NFT badges, and on-chain certificates as you learn. Sign up instantly (no wallet needed) and claim your unique ENS identity! {/* CTA Buttons */} @@ -129,7 +129,7 @@ export default function EthEdHero() {
- https://ethed.app + https://academy.eipsinsight.com
@@ -142,7 +142,7 @@ export default function EthEdHero() { -

EthEd Learning Platform

+

EIPsInsight Academy Learning Platform

Master Ethereum standards with AI-guided precision and verifiable rewards.

@@ -172,7 +172,7 @@ export default function EthEdHero() {
- {/* Floaties updated to match EthEd style */} + {/* Floaties updated to match EIPsInsight Academy style */}
@@ -197,7 +197,7 @@ export default function EthEdHero() { - + {/* How it works + Stats */} diff --git a/src/app/(public)/privacy/page.tsx b/src/app/(public)/privacy/page.tsx index 3aa44e1..e579e77 100644 --- a/src/app/(public)/privacy/page.tsx +++ b/src/app/(public)/privacy/page.tsx @@ -54,7 +54,7 @@ export default function PrivacyPage() {

5. Contact Us

- If you have any questions about this Privacy Policy, please contact us at privacy@ethed.app. + If you have any questions about this Privacy Policy, please contact us at privacy@eipsinsight.com.

diff --git a/src/app/(public)/terms/page.tsx b/src/app/(public)/terms/page.tsx index 3cc0d78..af5e186 100644 --- a/src/app/(public)/terms/page.tsx +++ b/src/app/(public)/terms/page.tsx @@ -14,42 +14,42 @@ export default function TermsPage() {

1. Acceptance of Terms

- By accessing or using EthEd, you agree to be bound by these Terms of Service and all applicable laws and regulations. If you do not agree with any of these terms, you are prohibited from using this site. + By accessing or using EIPsInsight Academy, you agree to be bound by these Terms of Service and all applicable laws and regulations. If you do not agree with any of these terms, you are prohibited from using this site.

2. Use License

- Permission is granted to temporarily use the materials on EthEd for personal, non-commercial transitory learning purposes only. + Permission is granted to temporarily use the materials on EIPsInsight Academy for personal, non-commercial transitory learning purposes only.

3. Blockchain Interactions

- EthEd facilitates interactions with the Polygon blockchain. You are responsible for any gas fees associated with on-chain transactions unless stated otherwise. We are not responsible for any losses resulting from blockchain network issues or wallet misconfigurations. + EIPsInsight Academy facilitates interactions with the Polygon blockchain. You are responsible for any gas fees associated with on-chain transactions unless stated otherwise. We are not responsible for any losses resulting from blockchain network issues or wallet misconfigurations.

4. NFTs and Digital Assets

- NFTs earned on EthEd are for educational recognition and personal collection. They do not represent financial investments or equity in EthEd. + NFTs earned on EIPsInsight Academy are for educational recognition and personal collection. They do not represent financial investments or equity in EIPsInsight Academy.

5. Disclaimer

- The materials on EthEd are provided on an 'as is' basis. We make no warranties, expressed or implied, and hereby disclaim all other warranties including, without limitation, implied warranties or conditions of merchantability or fitness for a particular purpose. + The materials on EIPsInsight Academy are provided on an 'as is' basis. We make no warranties, expressed or implied, and hereby disclaim all other warranties including, without limitation, implied warranties or conditions of merchantability or fitness for a particular purpose.

6. Governing Law

- These terms and conditions are governed by and construed in accordance with the laws of the jurisdiction in which EthEd operates. + These terms and conditions are governed by and construed in accordance with the laws of the jurisdiction in which EIPsInsight Academy operates.

diff --git a/src/app/403/page.tsx b/src/app/403/page.tsx index 9c797e9..37496a6 100644 --- a/src/app/403/page.tsx +++ b/src/app/403/page.tsx @@ -3,7 +3,7 @@ import Link from 'next/link'; import { ShieldOff } from 'lucide-react'; export const metadata: Metadata = { - title: 'Access Forbidden | EthEd', + title: 'Access Forbidden | EIPsInsight Academy', robots: { index: false, follow: false }, }; diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index eab4ab5..6dc1117 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -5,10 +5,10 @@ import Link from 'next/link'; import { Button } from '@/components/ui/button'; export const metadata: Metadata = { - title: 'About | EthEd', - description: 'EthEd is a Web3 education platform making blockchain learning accessible, interactive, and rewarding.', + title: 'About | EIPsInsight Academy', + description: 'EIPsInsight Academy is a Web3 education platform making blockchain learning accessible, interactive, and rewarding.', openGraph: { - title: 'About EthEd', + title: 'About EIPsInsight Academy', description: 'Making blockchain learning accessible, interactive, and rewarding.', }, }; @@ -49,9 +49,9 @@ const AboutPage = () => {
-

About EthEd

+

About EIPsInsight Academy

- EthEd is a decentralized learning platform designed to empower the next generation of blockchain developers and enthusiasts. We believe education should be accessible, verifiable, and rewarding. + EIPsInsight Academy is a decentralized learning platform designed to empower the next generation of blockchain developers and enthusiasts. We believe education should be accessible, verifiable, and rewarding.

@@ -69,7 +69,7 @@ const AboutPage = () => {

Community Owned

- EthEd is built on decentralized principles. Your data, your identity, and your learning path are controlled by you. + EIPsInsight Academy is built on decentralized principles. Your data, your identity, and your learning path are controlled by you.

diff --git a/src/app/admin/nfts/page.tsx b/src/app/admin/nfts/page.tsx index a446808..a157b3e 100644 --- a/src/app/admin/nfts/page.tsx +++ b/src/app/admin/nfts/page.tsx @@ -178,7 +178,7 @@ export default function AdminNFTsPage() { {nft.mintedAt ? new Date(nft.mintedAt).toLocaleDateString() : '—'} - {nft.transactionHash ? ( + {nft.transactionHash && nft.transactionHash.length > 2 && !/^0x0+$/.test(nft.transactionHash) ? ( 50, diff --git a/src/app/api/instructor/courses/[courseId]/route.ts b/src/app/api/instructor/courses/[courseId]/route.ts index fe60e11..b3d3776 100644 --- a/src/app/api/instructor/courses/[courseId]/route.ts +++ b/src/app/api/instructor/courses/[courseId]/route.ts @@ -87,7 +87,6 @@ export async function PUT( ...parse.data, level: parse.data.level as any, status: "DRAFT", // Reset to draft if it was rejected - rejectionReason: null, }, }); diff --git a/src/app/api/instructor/courses/[courseId]/submit/route.ts b/src/app/api/instructor/courses/[courseId]/submit/route.ts index ba6c3e7..75d8ca8 100644 --- a/src/app/api/instructor/courses/[courseId]/submit/route.ts +++ b/src/app/api/instructor/courses/[courseId]/submit/route.ts @@ -40,7 +40,7 @@ export async function POST( const updated = await prisma.course.update({ where: { id: courseId }, - data: { status: "AWAITING_APPROVAL", rejectionReason: null }, + data: { status: "AWAITING_APPROVAL" }, }); await createAuditLog({ diff --git a/src/app/api/og/profile/[id]/route.tsx b/src/app/api/og/profile/[id]/route.tsx index 48b8665..44ef52a 100644 --- a/src/app/api/og/profile/[id]/route.tsx +++ b/src/app/api/og/profile/[id]/route.tsx @@ -85,11 +85,11 @@ export async function GET( fontWeight: 'bold', }} > - EthEd Pioneer + EIPsInsight Academy Pioneer
- ethed.app + academy.eipsinsight.com
), diff --git a/src/app/api/payments/siwe/challenge/route.ts b/src/app/api/payments/siwe/challenge/route.ts index e2e3073..2de7a78 100644 --- a/src/app/api/payments/siwe/challenge/route.ts +++ b/src/app/api/payments/siwe/challenge/route.ts @@ -34,7 +34,7 @@ export async function POST(request: NextRequest) { `. Payment ID: ${nonce}`; // Build a proper SIWE message - const origin = request.headers.get("origin") || "https://ethed.app"; + const origin = request.headers.get("origin") || "https://academy.eipsinsight.com"; const domain = new URL(origin).host; const siweMessage = new SiweMessage({ diff --git a/src/app/api/user/nft/mint/route.ts b/src/app/api/user/nft/mint/route.ts index de62504..2a16f12 100644 --- a/src/app/api/user/nft/mint/route.ts +++ b/src/app/api/user/nft/mint/route.ts @@ -10,7 +10,27 @@ export async function POST(request: NextRequest) { if (errorResponse) return errorResponse; const body = await request.json(); - const { courseSlug, courseName, userAddress } = body; + const { courseSlug, courseName, userAddress: providedAddress } = body; + + // derive wallet address if not provided + let userAddress: string | undefined = providedAddress; + if (!userAddress) { + const userWithWallets = await prisma.user.findUnique({ + where: { id: session.user.id }, + include: { wallets: true }, + }); + if (userWithWallets && userWithWallets.wallets.length > 0) { + userAddress = + userWithWallets.wallets.find((w) => w.isPrimary)?.address || + userWithWallets.wallets[0]?.address || + undefined; + } + } + + if (!userAddress) { + // no wallet available - cannot mint on chain + logger.warn(`No wallet address available for user ${session.user.id}`, "api/nft/mint"); + } if (!courseSlug || !courseName) { return NextResponse.json( @@ -55,7 +75,7 @@ export async function POST(request: NextRequest) { userId: session.user.id, courseSlug, courseName, - userAddress, + userAddress, // may be undefined, mintCourseCompletionNFT handles off-chain fallback recipientName: session.user.name || undefined }); diff --git a/src/app/api/user/nft/share/route.ts b/src/app/api/user/nft/share/route.ts index 7c43477..ceebef2 100644 --- a/src/app/api/user/nft/share/route.ts +++ b/src/app/api/user/nft/share/route.ts @@ -50,11 +50,11 @@ export async function POST(request: Request) { description: nft.metadata && typeof nft.metadata === "object" && "description" in nft.metadata ? (nft.metadata as Record).description - : `${nft.name} – earned on EthEd`, + : `${nft.name} – earned at EIPsInsight Academy`, image: nft.image, tokenId: nft.tokenId, owner: session.user.id, - platform: "EthEd", + platform: "EIPsInsight Academy", sharedAt: new Date().toISOString(), originalMetadata: nft.metadata, }; diff --git a/src/app/api/user/wallets/route.ts b/src/app/api/user/wallets/route.ts index 1e58aff..baf6270 100644 --- a/src/app/api/user/wallets/route.ts +++ b/src/app/api/user/wallets/route.ts @@ -4,6 +4,7 @@ import { authOptions } from "@/lib/auth"; import { prisma } from "@/lib/prisma-client"; import aj, { slidingWindow } from "@/lib/arcjet"; import { AMOY_CHAIN_ID } from "@/lib/contracts"; +import { logger } from "@/lib/monitoring"; // Rate limiting for wallet connections const walletRateLimit = aj.withRule( @@ -35,7 +36,8 @@ export async function GET() { return NextResponse.json({ wallets }); - } catch { + } catch (err) { + logger.error("GET /api/user/wallets failed", "api/user/wallets", undefined, err); return NextResponse.json( { error: "Internal server error" }, { status: 500 } diff --git a/src/app/community/layout.tsx b/src/app/community/layout.tsx index 257f35c..612380b 100644 --- a/src/app/community/layout.tsx +++ b/src/app/community/layout.tsx @@ -1,13 +1,13 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Community | EthEd", + title: "Community | EIPsInsight Academy", description: - "Join the EthEd community of Web3 learners, builders, and educators. Collaborate, earn rewards, and grow together.", + "Join the EIPsInsight Academy community of Web3 learners, builders, and educators. Collaborate, earn rewards, and grow together.", openGraph: { - title: "Community | EthEd", + title: "Community | EIPsInsight Academy", description: - "Join the EthEd community of Web3 learners, builders, and educators.", + "Join the EIPsInsight Academy community of Web3 learners, builders, and educators.", }, }; diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx index 65cfc48..c4a69f1 100644 --- a/src/app/community/page.tsx +++ b/src/app/community/page.tsx @@ -89,7 +89,7 @@ export default function CommunityPage() { { date: "March 2024", title: "The Spark of an Idea", - description: "EthEd was born at ETHGlobal London, where our founder realized that Web3 education needed a more personalized, AI-driven approach.", + description: "EIPsInsight Academy was born at ETHGlobal London, where our founder realized that Web3 education needed a more personalized, AI-driven approach.", icon: Lightbulb, color: "emerald", ethGlobalEvent: "ETHGlobal London 2024", @@ -116,7 +116,7 @@ export default function CommunityPage() { { date: "May 2024", title: "European Expansion", - description: "ETHGlobal Brussels was where EthEd truly came alive. We launched our ENS integration and saw our first real user growth.", + description: "ETHGlobal Brussels was where EIPsInsight Academy truly came alive. We launched our ENS integration and saw our first real user growth.", icon: Globe, color: "blue", ethGlobalEvent: "ETHGlobal Brussels 2024", @@ -130,7 +130,7 @@ export default function CommunityPage() { { date: "July 2024", title: "Community Growth", - description: "Summer brought incredible growth as word spread through the Web3 community. Developers from around the world started joining EthEd.", + description: "Summer brought incredible growth as word spread through the Web3 community. Developers from around the world started joining EIPsInsight Academy.", icon: Users, color: "purple", achievements: [ @@ -151,13 +151,13 @@ export default function CommunityPage() { "🥇 Winner - Best Educational Platform", "💰 $15,000 prize + $500K Series A", "📰 Featured in TechCrunch & CoinDesk", - "🚀 Launched EthEd free learning platform" + "🚀 Launched EIPsInsight Academy free learning platform" ] }, { date: "Present Day", title: "Building the Future", - description: "Today, EthEd is the leading Web3 education platform. With AI companions, verified credentials, and a thriving community, we're just getting started.", + description: "Today, EIPsInsight Academy is the leading Web3 education platform. With AI companions, verified credentials, and a thriving community, we're just getting started.", icon: Star, color: "purple", achievements: [ @@ -339,7 +339,7 @@ export default function CommunityPage() {

- The EthEd Journey + The EIPsInsight Academy Journey

@@ -657,7 +657,8 @@ export default function CommunityPage() { - {n.transactionHash && } + {n.transactionHash && n.transactionHash.length > 2 && !/^0x0+$/.test(n.transactionHash) && } diff --git a/src/app/donate/layout.tsx b/src/app/donate/layout.tsx index 134c0e7..16c837d 100644 --- a/src/app/donate/layout.tsx +++ b/src/app/donate/layout.tsx @@ -1,13 +1,13 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Donate | EthEd", + title: "Donate | EIPsInsight Academy", description: - "Support Web3 education by donating to EthEd. Help make blockchain learning accessible to everyone.", + "Support Web3 education by donating to EIPsInsight Academy. Help make blockchain learning accessible to everyone.", openGraph: { - title: "Support Web3 Education | EthEd", + title: "Support Web3 Education | EIPsInsight Academy", description: - "Support Web3 education by donating to EthEd.", + "Support Web3 education by donating to EIPsInsight Academy.", }, }; diff --git a/src/app/donate/page.tsx b/src/app/donate/page.tsx index 2f1ced2..7c82124 100644 --- a/src/app/donate/page.tsx +++ b/src/app/donate/page.tsx @@ -281,7 +281,7 @@ function DonateContent() {

- Support EthEd with any cryptocurrency, on any chain. Every contribution helps us + Support EIPsInsight Academy with any cryptocurrency, on any chain. Every contribution helps us build free, open-source blockchain education.

diff --git a/src/app/how-it-works/layout.tsx b/src/app/how-it-works/layout.tsx index 9aed2a3..54eeca5 100644 --- a/src/app/how-it-works/layout.tsx +++ b/src/app/how-it-works/layout.tsx @@ -1,13 +1,13 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "How It Works | EthEd", + title: "How It Works | EIPsInsight Academy", description: - "Learn how EthEd works — from signing up to earning NFT certificates for completing Web3 courses.", + "Learn how EIPsInsight Academy works — from signing up to earning NFT certificates for completing Web3 courses.", openGraph: { - title: "How It Works | EthEd", + title: "How It Works | EIPsInsight Academy", description: - "Learn how EthEd works — from signing up to earning NFT certificates.", + "Learn how EIPsInsight Academy works — from signing up to earning NFT certificates.", }, }; diff --git a/src/app/how-it-works/page.tsx b/src/app/how-it-works/page.tsx index fe0b28a..be8b22e 100644 --- a/src/app/how-it-works/page.tsx +++ b/src/app/how-it-works/page.tsx @@ -182,7 +182,7 @@ export default function HowItWorksPage() { transition={{ duration: 0.8, delay: 0.1 }} className="text-4xl md:text-5xl font-bold tracking-tight mb-6 bg-gradient-to-r from-cyan-400 via-purple-400 to-emerald-400 bg-clip-text text-transparent" > - How EthEd Works + How EIPsInsight Academy Works - Discover how EthEd revolutionizes Web3 education with permanent + Discover how EIPsInsight Academy revolutionizes Web3 education with permanent blockchain credentials and hands-on learning experiences. diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 99f2a61..4333ccf 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -30,26 +30,26 @@ const merriweather = Merriweather({ display: "swap", }); -const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://ethed.com"; +const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://academy.eipsinsight.com"; export const metadata: Metadata = { metadataBase: new URL(siteUrl), - title: "EthEd - Master Blockchain and Web3", - description: "EthEd makes blockchain and Web3 education fun, verifiable, and rewarding. Earn NFTs, badges, and real progress while learning with a built-in AI tutor!", + title: "EIPsInsight Academy - Master Blockchain and Web3", + description: "EIPsInsight Academy makes blockchain and Web3 education fun, verifiable, and rewarding. Earn NFTs, badges, and real progress while learning with a built-in AI tutor!", alternates: { canonical: "/", }, openGraph: { - title: "EthEd - Master Blockchain and Web3", - description: "EthEd makes blockchain and Web3 education fun, verifiable, and rewarding. Earn NFTs, badges, and real progress while learning with a built-in AI tutor!", + title: "EIPsInsight Academy - Master Blockchain and Web3", + description: "EIPsInsight Academy makes blockchain and Web3 education fun, verifiable, and rewarding. Earn NFTs, badges, and real progress while learning with a built-in AI tutor!", url: siteUrl, - siteName: "EthEd", + siteName: "EIPsInsight Academy", images: [ { url: `${siteUrl}/og-image.png`, width: 1200, height: 630, - alt: "EthEd - Master Blockchain and Web3", + alt: "EIPsInsight Academy - Master Blockchain and Web3", }, ], locale: "en_US", @@ -57,8 +57,8 @@ export const metadata: Metadata = { }, twitter: { card: "summary_large_image", - title: "EthEd - Master Blockchain and Web3", - description: "EthEd makes blockchain and Web3 education fun, verifiable, and rewarding. Earn NFTs, badges, and real progress while learning with a built-in AI tutor!", + title: "EIPsInsight Academy - Master Blockchain and Web3", + description: "EIPsInsight Academy makes blockchain and Web3 education fun, verifiable, and rewarding. Earn NFTs, badges, and real progress while learning with a built-in AI tutor!", images: [`${siteUrl}/og-image.png`], }, }; @@ -73,8 +73,8 @@ export default function RootLayout({ diff --git a/src/app/leaderboard/layout.tsx b/src/app/leaderboard/layout.tsx index 31504cb..5d546dc 100644 --- a/src/app/leaderboard/layout.tsx +++ b/src/app/leaderboard/layout.tsx @@ -1,13 +1,13 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Leaderboard | EthEd", + title: "Leaderboard | EIPsInsight Academy", description: - "See the top Web3 learners on EthEd. Earn XP by completing courses and climb the ranks.", + "See the top Web3 learners on EIPsInsight Academy. Earn XP by completing courses and climb the ranks.", openGraph: { - title: "Leaderboard | EthEd", + title: "Leaderboard | EIPsInsight Academy", description: - "See the top Web3 learners on EthEd. Earn XP and climb the ranks.", + "See the top Web3 learners on EIPsInsight Academy. Earn XP and climb the ranks.", }, }; diff --git a/src/app/leaderboard/page.tsx b/src/app/leaderboard/page.tsx index 67e74b8..610153b 100644 --- a/src/app/leaderboard/page.tsx +++ b/src/app/leaderboard/page.tsx @@ -70,7 +70,7 @@ export default function LeaderboardPage() { Global Explorer Rankings

- Celebrating the top contributors and lifelong learners in the EthEd ecosystem. + Celebrating the top contributors and lifelong learners in the EIPsInsight Academy ecosystem.

diff --git a/src/app/learn/layout.tsx b/src/app/learn/layout.tsx index 633b4c2..51fbf40 100644 --- a/src/app/learn/layout.tsx +++ b/src/app/learn/layout.tsx @@ -1,11 +1,11 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Learn | EthEd", + title: "Learn | EIPsInsight Academy", description: - "Explore Web3 courses on blockchain, Solidity, DeFi, NFTs, and more. Learn at your own pace with hands-on projects.", + "Explore Web3 courses on blockchain, Solidity, DeFi, NFTs, and more. Learn at your own pace with hands-on projects provided by EIPsInsight Academy.", openGraph: { - title: "Learn Web3 & Blockchain | EthEd", + title: "Learn Web3 & Blockchain | EIPsInsight Academy", description: "Explore Web3 courses on blockchain, Solidity, DeFi, NFTs, and more.", }, diff --git a/src/app/nft/[id]/page.tsx b/src/app/nft/[id]/page.tsx index 670f0b7..c2df1b0 100644 --- a/src/app/nft/[id]/page.tsx +++ b/src/app/nft/[id]/page.tsx @@ -25,8 +25,8 @@ export async function generateMetadata({ params }: NFTPageProps): Promise : {}; - const description = (metadata.description as string) || `${nft.name} — earned on EthEd`; + const description = (metadata.description as string) || `${nft.name} — earned at EIPsInsight Academy`; const txHash = (nft as unknown as { transactionHash?: string | null }).transactionHash ?? null; const chainId = (nft as unknown as { chainId?: number | null }).chainId ?? null; - const explorerUrl = txHash && chainId && !txHash.startsWith('0x' + '0'.repeat(64)) + const explorerUrl = txHash && txHash.length > 2 && chainId && !/^0x0+$/.test(txHash) ? getExplorerTxUrl(chainId, txHash) : null; @@ -64,7 +64,7 @@ export default async function NFTPublicPage({ params }: NFTPageProps) {
{/* Back */} {/* NFT Card */} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index dbb6a66..a8ed153 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Home, ArrowLeft, Search } from 'lucide-react'; export const metadata: Metadata = { - title: 'Page Not Found | EthEd', + title: 'Page Not Found | EIPsInsight Academy', }; export default function NotFound() { diff --git a/src/app/onboarding/_components/Onboarding.tsx b/src/app/onboarding/_components/Onboarding.tsx index 7bd3fe4..15a779a 100644 --- a/src/app/onboarding/_components/Onboarding.tsx +++ b/src/app/onboarding/_components/Onboarding.tsx @@ -120,7 +120,7 @@ export default function Onboarding() { if (name.startsWith('-') || name.endsWith('-')) return { valid: false, message: "Cannot start or end with hyphen" }; if (name.includes('--')) return { valid: false, message: "Cannot contain consecutive hyphens" }; - const reserved = ['admin', 'api', 'www', 'mail', 'ftp', 'localhost', 'ethed', 'test']; + const reserved = ['admin', 'api', 'www', 'mail', 'ftp', 'localhost', 'eipsinsight', 'test']; if (reserved.includes(name)) return { valid: false, message: "This name is reserved" }; return { valid: true, message: "Valid ENS name" }; diff --git a/src/app/pricing/layout.tsx b/src/app/pricing/layout.tsx index 6ebc892..20a7ffe 100644 --- a/src/app/pricing/layout.tsx +++ b/src/app/pricing/layout.tsx @@ -1,12 +1,12 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Pricing | EthEd", + title: "Pricing | EIPsInsight Academy", description: - "EthEd pricing plans for Web3 and blockchain education. Start learning for free.", + "EIPsInsight Academy pricing plans for Web3 and blockchain education. Start learning for free.", openGraph: { - title: "Pricing | EthEd", - description: "EthEd pricing plans. Start learning for free.", + title: "Pricing | EIPsInsight Academy", + description: "EIPsInsight Academy pricing plans. Start learning for free.", }, }; diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index bb00cb6..3f08802 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -53,7 +53,7 @@ export default function PricingPage() { Invest in Your Decentralized Future

- EthEd is currently in Early Access. All existing courses are free to celebrate our launch. + EIPsInsight Academy is currently in Early Access. All existing courses are free to celebrate our launch.

diff --git a/src/app/profile/[id]/page.tsx b/src/app/profile/[id]/page.tsx index b98b136..cfbaa4f 100644 --- a/src/app/profile/[id]/page.tsx +++ b/src/app/profile/[id]/page.tsx @@ -32,8 +32,8 @@ export async function generateMetadata({ params }: ProfilePageProps): PromiseNFT

{nft.description || (nft.metadata?.description ?? '')}

- +
+ Minted: {nft.createdAt ? new Date(nft.createdAt).toLocaleDateString() : ''} +
diff --git a/src/app/projects/layout.tsx b/src/app/projects/layout.tsx index 906ceb5..ffe3d7e 100644 --- a/src/app/projects/layout.tsx +++ b/src/app/projects/layout.tsx @@ -1,13 +1,13 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Projects | EthEd", + title: "Projects | EIPsInsight Academy", description: - "Browse and contribute to open-source Web3 projects built by the EthEd community.", + "Browse and contribute to open-source Web3 projects built by the EIPsInsight Academy community.", openGraph: { - title: "Projects | EthEd", + title: "Projects | EIPsInsight Academy", description: - "Browse and contribute to open-source Web3 projects built by the EthEd community.", + "Browse and contribute to open-source Web3 projects built by the EIPsInsight Academy community.", }, }; diff --git a/src/app/robots.ts b/src/app/robots.ts index d5b6487..66f6846 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -1,6 +1,6 @@ import type { MetadataRoute } from "next"; -const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://ethed.com"; +const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://academy.eipsinsight.com"; export default function robots(): MetadataRoute.Robots { return { diff --git a/src/app/settings/layout.tsx b/src/app/settings/layout.tsx index 4752ad3..09f665e 100644 --- a/src/app/settings/layout.tsx +++ b/src/app/settings/layout.tsx @@ -1,8 +1,8 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Settings | EthEd", - description: "Manage your EthEd account settings, notifications, and preferences.", + title: "Settings | EIPsInsight Academy", + description: "Manage your EIPsInsight Academy account settings, notifications, and preferences.", robots: { index: false, follow: false }, }; diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 7c4dbe0..62b89b9 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,6 +1,6 @@ import type { MetadataRoute } from "next"; -const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://ethed.com"; +const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://academy.eipsinsight.com"; const publicRoutes = [ { path: "", priority: 1.0, changeFrequency: "weekly" as const }, diff --git a/src/components/CourseModulePage.tsx b/src/components/CourseModulePage.tsx index 368e5e7..e5d5b14 100644 --- a/src/components/CourseModulePage.tsx +++ b/src/components/CourseModulePage.tsx @@ -20,7 +20,8 @@ import { Users, Trophy, Clock, - Target + Target, + Lock } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import EnhancedLessonViewer from './EnhancedLessonViewer'; @@ -113,6 +114,13 @@ export default function CourseModulePage({ onProgress?.(progressPercentage); }, [progressPercentage, onProgress]); + // Scroll to top whenever a new lesson is selected + useEffect(() => { + if (selectedLesson) { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [selectedLesson]); + const handleLessonComplete = (lessonId: string) => { const newCompleted = [...new Set([...completedLessons, lessonId])]; setCompletedLessons(newCompleted); @@ -121,6 +129,7 @@ export default function CourseModulePage({ fetch('/api/user/course/progress', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify({ courseSlug: courseId, completedCount: newCompleted.length, @@ -272,23 +281,37 @@ export default function CourseModulePage({ completedLessons.includes(l.id) ).length / module.lessons.length * 100; + // Module is unlocked if it's the first one, or all lessons + // in the preceding module are completed + const isModuleUnlocked = index === 0 || + modules[index - 1].lessons.every(l => completedLessons.includes(l.id)); + return ( { - setSelectedModule(module); - setSelectedLesson(null); + if (isModuleUnlocked) { + setSelectedModule(module); + setSelectedLesson(null); + } else { + import('sonner').then(({ toast }) => + toast.error('Complete the previous module first!') + ); + } }} - whileHover={{ x: 4 }} + whileHover={{ x: isModuleUnlocked ? 4 : 0 }} className={`w-full p-3 rounded-lg text-left transition-all border ${ - selectedModule.id === module.id - ? 'bg-cyan-600/20 border-cyan-400/50' - : 'bg-slate-800/30 border-slate-700 hover:border-slate-600' + !isModuleUnlocked + ? 'bg-slate-800/20 border-slate-800 opacity-50 cursor-not-allowed' + : selectedModule.id === module.id + ? 'bg-cyan-600/20 border-cyan-400/50' + : 'bg-slate-800/30 border-slate-700 hover:border-slate-600' }`} >
+ {!isModuleUnlocked && } {module.icon || '📚'} -

+

{module.title}

@@ -357,6 +380,12 @@ export default function CourseModulePage({ {selectedModule.lessons.map((lesson, index) => { const isCompleted = completedLessons.includes(lesson.id); + // A lesson is unlocked if it's the first in the module, + // or all preceding lessons in this module are completed + const isUnlocked = index === 0 || + selectedModule.lessons + .slice(0, index) + .every(prev => completedLessons.includes(prev.id)); return ( setSelectedLesson(lesson)} - className={`cursor-pointer transition-all border ${ - isCompleted - ? 'bg-slate-900/60 border-emerald-400/30 hover:border-emerald-400/50' - : 'bg-slate-900/60 border-slate-700 hover:border-cyan-400/50' + onClick={() => { + if (isUnlocked) { + setSelectedLesson(lesson); + } else { + // Optionally show a toast or do nothing + import('sonner').then(({ toast }) => + toast.error('Complete the previous lessons first!') + ); + } + }} + className={`transition-all border ${ + !isUnlocked + ? 'bg-slate-900/30 border-slate-800 opacity-60 cursor-not-allowed' + : isCompleted + ? 'bg-slate-900/60 border-emerald-400/30 hover:border-emerald-400/50 cursor-pointer' + : 'bg-slate-900/60 border-slate-700 hover:border-cyan-400/50 cursor-pointer' }`} > @@ -381,6 +421,9 @@ export default function CourseModulePage({ {isCompleted && ( )} + {!isUnlocked && !isCompleted && ( + + )} {lesson.type === 'reading' && '📖'} {lesson.type === 'video' && '🎥'} @@ -390,6 +433,7 @@ export default function CourseModulePage({ {lesson.type === 'discussion' && '💬'}

Lesson {index + 1}: {lesson.title} @@ -401,9 +445,12 @@ export default function CourseModulePage({ - + {isUnlocked + ? + : + } diff --git a/src/components/EnhancedLessonViewer.tsx b/src/components/EnhancedLessonViewer.tsx index 42697da..8ce5558 100644 --- a/src/components/EnhancedLessonViewer.tsx +++ b/src/components/EnhancedLessonViewer.tsx @@ -1,7 +1,9 @@ 'use client'; import React, { useState, useEffect } from 'react'; +import { useSession } from "next-auth/react"; import { ArrowLeft, ArrowRight, CheckCircle, Clock, Zap, BookOpen, BarChart3, Trophy, Award } from 'lucide-react'; +import { isOnChainEnabled } from '@/lib/viem-client'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; @@ -197,6 +199,8 @@ export default function EnhancedLessonViewer({ const [quizAnswers, setQuizAnswers] = useState>({}); const [showQuiz, setShowQuiz] = useState(false); const [quizScore, setQuizScore] = useState(null); + const { data: session } = useSession(); + const [showCelebration, setShowCelebration] = useState(false); const [noteContent, setNoteContent] = useState(''); const [showNotes, setShowNotes] = useState(false); @@ -209,6 +213,8 @@ export default function EnhancedLessonViewer({ setShowQuiz(false); setQuizScore(null); setNoteContent(''); + // Scroll to top when navigating to a new lesson + window.scrollTo({ top: 0, behavior: 'smooth' }); }, [lesson.id, courseContext.completedLessons]); const lessonProgress = ((courseContext.completedLessons.length + (isCompleted ? 0 : 1)) / courseContext.totalLessons) * 100; @@ -230,38 +236,80 @@ export default function EnhancedLessonViewer({ handleMarkComplete(); } - // Wait a moment for progress sync to DB - await new Promise(r => setTimeout(r, 1000)); + // require wallet for on-chain minting if the feature flag is enabled + if (isOnChainEnabled() && !session?.address) { + toast.error( + "Please connect a wallet before finishing the course.", + { description: "A wallet is required to mint the NFT on-chain." } + ); + setIsFinishing(false); + return; + } + // Sync final progress to the backend before minting + if (!session?.address) { + // let user know that without a connected wallet the NFT will be off-chain + toast.warn( + "No wallet connected — NFT will be recorded off-chain.", + { description: "Connect a wallet if you want the certificate minted on Polygon." } + ); + } + try { + await fetch('/api/user/course/progress', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + courseSlug: courseContext.courseId, + completedCount: courseContext.totalLessons, + totalModules: courseContext.totalLessons, + completedModules: [ + ...courseContext.completedLessons.map(String), + String(lesson.id) + ].filter((v, i, a) => a.indexOf(v) === i) + }) + }); + } catch (err) { + logger.error('Failed to sync final progress', 'EnhancedLessonViewer', undefined, err); + } try { toast.promise( fetch('/api/user/nft/mint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify({ courseSlug: courseContext.courseId, courseName: courseContext.courseName, - // If you have a wallet address available in context, pass it - // userAddress: userWalletAddress + // include a connected wallet address if session has one + userAddress: session?.address || undefined, }) }).then(async res => { - if (!res.ok) throw new Error('Minting failed'); const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Minting failed'); return data; }), { loading: 'Finalizing course & minting your NFT...', success: (data) => { - setTimeout(() => router.push('/profile'), 2000); - return data.alreadyMinted - ? 'Achievement updated! Redirecting...' - : 'Course Complete! Your NFT is being minted! 🎓'; + setTimeout(() => router.push('/dashboard'), 2000); + if (data.alreadyMinted) { + return 'Achievement updated! Redirecting...'; + } + if (!data.transaction || !data.transaction.txHash) { + return 'Course Complete! Certificate recorded (off-chain).'; + } + return 'Course Complete! Your NFT is being minted! 🎓'; }, - error: 'Course completed, but NFT minting encountered an issue.' + error: (err) => { + setIsFinishing(false); + return `Course completed, but NFT minting encountered an issue: ${err instanceof Error ? err.message : 'Unknown error'}`; + } } ); } catch (error) { logger.error('Finalization error', 'EnhancedLessonViewer', undefined, error); + setIsFinishing(false); router.push('/dashboard'); } }; diff --git a/src/components/LessonViewer.tsx b/src/components/LessonViewer.tsx index 3ee181a..5710e96 100644 --- a/src/components/LessonViewer.tsx +++ b/src/components/LessonViewer.tsx @@ -609,7 +609,7 @@ Let's apply this framework to EIP-1559: content: ` # Draft Your First EIP: Hands-On Workshop -Now it's time to put everything together and draft your first EIP using EthEd's Proposal Builder. +Now it's time to put everything together and draft your first EIP using EIPsInsight Academy's Proposal Builder. ## Workshop Overview @@ -668,7 +668,7 @@ In this hands-on session, we'll: ## Step 4: Writing Your EIP (10 minutes) -### Using EthEd's Proposal Builder +### Using EIPsInsight Academy's Proposal Builder The Proposal Builder provides: - **Template Generation**: Automatically creates proper EIP structure @@ -748,7 +748,7 @@ You've mastered: - ✅ Understanding Ethereum's architecture and EIP system - ✅ Reading and analyzing existing EIPs - ✅ Writing well-structured proposals -- ✅ Using EthEd's tools for EIP development +- ✅ Using EIPsInsight Academy's tools for EIP development - ✅ Engaging with the Ethereum community **Ready to claim your EIP Expert NFT badge!** diff --git a/src/components/forms/CourseCreationForm.tsx b/src/components/forms/CourseCreationForm.tsx index 3c53ca4..7015711 100644 --- a/src/components/forms/CourseCreationForm.tsx +++ b/src/components/forms/CourseCreationForm.tsx @@ -148,7 +148,7 @@ export default function CourseCreationForm({ {courseId ? "Update course details and settings" - : "Add a new course to eth.ed"} + : "Add a new course to EIPsInsight Academy"} diff --git a/src/components/forms/ProfileSetupForm.tsx b/src/components/forms/ProfileSetupForm.tsx index cf00957..854e94c 100644 --- a/src/components/forms/ProfileSetupForm.tsx +++ b/src/components/forms/ProfileSetupForm.tsx @@ -96,7 +96,7 @@ export default function ProfileSetupForm({ Setup Your Profile - Personalize your eth.ed experience + Personalize your EIPsInsight Academy experience diff --git a/src/components/logo.tsx b/src/components/logo.tsx index 1066fea..8e938e9 100644 --- a/src/components/logo.tsx +++ b/src/components/logo.tsx @@ -1,16 +1,43 @@ -import Image from "next/image"; - export default function Logo() { return (
- eth.ed + + {/* Main text: EIPsInsight - big, bold, italic */} + + EIPsInsight + + + {/* Subtitle: Academy - small */} + + ACADEMY + +
); } diff --git a/src/components/nft-share-modal.tsx b/src/components/nft-share-modal.tsx index 5b8d530..1c3520c 100644 --- a/src/components/nft-share-modal.tsx +++ b/src/components/nft-share-modal.tsx @@ -65,7 +65,7 @@ export default function NFTShareModal({ nft, open, onClose }: NFTShareModalProps }; const handleTwitterShare = () => { - const text = encodeURIComponent(`I just earned the "${courseName}" NFT on @ethed_app! 🎉🏆\n\nLearn blockchain & earn verifiable NFT credentials:\n${shareUrl}`); + const text = encodeURIComponent(`I just earned the "${courseName}" NFT from EIPsInsight Academy! 🎉🏆\n\nLearn blockchain & earn verifiable NFT credentials:\n${shareUrl}`); window.open(`https://twitter.com/intent/tweet?text=${text}`, '_blank'); }; @@ -153,13 +153,21 @@ export default function NFTShareModal({ nft, open, onClose }: NFTShareModalProps {isPinning ? 'Pinning to IPFS...' : 'Pin Metadata to IPFS'} - {explorerUrl && ( + {explorerUrl ? ( + ) : ( + // if there's no valid tx hash or chain info, show a hint so users + // aren’t left wondering where the verify link went +
+ {isRealTx + ? 'No chain info available yet.' + : 'NFT has not been minted on-chain or transaction data is missing.'} +
)} {ipfsUrl && ( diff --git a/src/components/seo/JsonLd.tsx b/src/components/seo/JsonLd.tsx index 622107c..2c6493c 100644 --- a/src/components/seo/JsonLd.tsx +++ b/src/components/seo/JsonLd.tsx @@ -12,10 +12,10 @@ interface OrganizationJsonLdProps { * Organization structured data for SEO */ export function OrganizationJsonLd({ - name = 'EthEd', - url = 'https://ethed.com', - logo = 'https://ethed.com/logo.png', - description = 'EthEd makes blockchain and Web3 education fun, verifiable, and rewarding.', + name = 'EIPsInsight Academy', + url = 'https://academy.eipsinsight.com', + logo = 'https://academy.eipsinsight.com/logo.png', + description = 'EIPsInsight Academy makes blockchain and Web3 education fun, verifiable, and rewarding.', sameAs = [], }: OrganizationJsonLdProps) { const jsonLd = { @@ -54,7 +54,7 @@ interface CourseJsonLdProps { export function CourseJsonLd({ name, description, - provider = 'EthEd', + provider = 'EIPsInsight Academy', url, image, courseMode = 'Online', @@ -69,7 +69,7 @@ export function CourseJsonLd({ provider: { '@type': 'Organization', name: provider, - url: 'https://ethed.com', + url: 'https://academy.eipsinsight.com', }, url, ...(image && { image }), @@ -99,9 +99,9 @@ interface WebsiteJsonLdProps { * Website structured data with search action */ export function WebsiteJsonLd({ - name = 'EthEd', - url = 'https://ethed.com', - description = 'Master blockchain and Web3 with EthEd', + name = 'EIPsInsight Academy', + url = 'https://academy.eipsinsight.com', + description = 'Master blockchain and Web3 with EIPsInsight Academy', }: WebsiteJsonLdProps) { const jsonLd = { '@context': 'https://schema.org', diff --git a/src/components/sidebar/site-header.tsx b/src/components/sidebar/site-header.tsx index 1e4e20b..6fd4e20 100644 --- a/src/components/sidebar/site-header.tsx +++ b/src/components/sidebar/site-header.tsx @@ -11,7 +11,7 @@ export function SiteHeader() { orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" /> -

eth.ed

+

EIPsInsight Academy

) diff --git a/src/components/siwe-login-button.tsx b/src/components/siwe-login-button.tsx index bdf69e1..690e368 100644 --- a/src/components/siwe-login-button.tsx +++ b/src/components/siwe-login-button.tsx @@ -146,7 +146,7 @@ export function SiweLoginButton() { const message = new SiweMessage({ domain: window.location.host, address: address, - statement: "Sign in with Ethereum to eth.ed", + statement: "Sign in with Ethereum to EIPsInsight Academy", uri: window.location.origin, version: "1", chainId: chainId, diff --git a/src/lib/ai-client.ts b/src/lib/ai-client.ts index 73fbc0f..ecdea38 100644 --- a/src/lib/ai-client.ts +++ b/src/lib/ai-client.ts @@ -80,7 +80,7 @@ export function isInputSafe(message: string): boolean { // System prompt // --------------------------------------------------------------------------- -const SYSTEM_PROMPT = `You are the eth.ed AI Learning Assistant — a helpful, concise tutor for blockchain and Ethereum education. +const SYSTEM_PROMPT = `You are the EIPsInsight Academy AI Learning Assistant — a helpful, concise tutor for blockchain and Ethereum education. Guidelines: - Answer questions about Ethereum, EIPs, smart contracts, Web3, blockchain fundamentals, and related topics. diff --git a/src/lib/auth.ts b/src/lib/auth.ts index a35d885..7170da9 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -56,10 +56,10 @@ export const authOptions: NextAuthOptions = { return null; } // Secure admin credentials check (hardcoded for security, not in env) - if (credentials.email === "admin@ethed.com" && credentials.password === "ADMIN@2026") { + if (credentials.email === "admin@eipsinsight.com" && credentials.password === "ADMIN@2026") { return { - id: "admin@ethed.com", - email: "admin@ethed.com", + id: "admin@eipsinsight.com", + email: "admin@eipsinsight.com", name: "Admin", role: "ADMIN", // Set role here for immediate session access }; diff --git a/src/lib/certificate-generator.ts b/src/lib/certificate-generator.ts new file mode 100644 index 0000000..f387c97 --- /dev/null +++ b/src/lib/certificate-generator.ts @@ -0,0 +1,446 @@ +/** + * Certificate Generator + * + * Generates unique on-brand SVG certificates for every recipient. + * + * Uniqueness axes: + * 1. Background gradient — derived deterministically from wallet address + * (same technique used by ENS for avatar gradients). + * 2. Accent / border colour — one accent hue per course slug. + * 3. Recipient name — ENS subdomain or display name, embedded in the image. + * 4. Serial number — token ID or a short hash burned into the footer. + * 5. Completion date — embedded in the footer. + * + * Output: an SVG Buffer (image/svg+xml) ready to be uploaded to IPFS via Pinata. + * No external dependencies required — pure Node.js string generation. + */ + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type CertificateType = "course-completion" | "pioneer"; + +export interface CertificateParams { + type: CertificateType; + /** ENS name (e.g. "vitalik.ayushetty.eth") or fallback display name */ + recipientName: string; + /** Wallet address used to derive a unique colour palette */ + walletAddress?: string; + /** Human-readable course title */ + courseName?: string; + /** Course slug — used to pick the per-course accent hue */ + courseSlug?: string; + /** ISO date string of completion */ + completionDate: string; + /** e.g. "Beginner" */ + courseLevel?: string; + /** + * Token ID (or any short unique identifier) burned into the serial field. + * If omitted, a deterministic fallback is derived from the params. + */ + serialNumber?: string; +} + +// --------------------------------------------------------------------------- +// Colour helpers +// --------------------------------------------------------------------------- + +/** + * Derives a deterministic hue (0–359) from a wallet address string. + * Uses a simple djb2-style hash so the output is always the same for the + * same address — giving every user their own "colour identity". + */ +function addressToHue(address: string): number { + const clean = address.replace(/^0x/i, "").toLowerCase().padStart(40, "0"); + let hash = 5381; + for (let i = 0; i < clean.length; i++) { + hash = ((hash << 5) + hash + clean.charCodeAt(i)) | 0; + } + return Math.abs(hash) % 360; +} + +/** Build a CSS hsl() string */ +function hsl(h: number, s: number, l: number): string { + return `hsl(${h % 360},${s}%,${l}%)`; +} + +/** + * Per-course accent hues — each course has its own recognisable colour. + * Courses not listed fall back to the user's personal hue. + */ +const COURSE_ACCENT_HUES: Record = { + "eips-101": 220, // Blue — Ethereum Governance + "ens-101": 165, // Teal — Naming Service + "0g-101": 280, // Violet — AI Infrastructure + "blockchain-basics": 38, // Amber — Fundamentals + "solidity-dev": 200, // Cyan — Development + "defi-protocols": 145, // Green — DeFi + "nft-ecosystem": 320, // Pink — NFTs + "web3-security": 355, // Red — Security +}; + +/** Pioneer badge always uses a gold/amber accent */ +const PIONEER_ACCENT_HUE = 42; + +// --------------------------------------------------------------------------- +// XML sanitiser — prevent injected SVG/XML from user-supplied strings +// --------------------------------------------------------------------------- + +function xmlEscape(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** Truncate a string to maxLen characters, appending "…" if trimmed */ +function truncate(str: string, maxLen: number): string { + if (str.length <= maxLen) return str; + return str.slice(0, maxLen - 1) + "…"; +} + +// --------------------------------------------------------------------------- +// Serial helper +// --------------------------------------------------------------------------- + +function buildSerial( + serialNumber: string | undefined, + walletAddress: string, + courseName: string +): string { + if (serialNumber) { + // Prefer token ID — shorten long off-chain IDs like "off-172…xyz" + const cleaned = serialNumber.startsWith("off-") + ? serialNumber.slice(4, 12).toUpperCase() + : serialNumber.slice(0, 8).toUpperCase(); + return `#${cleaned}`; + } + // Deterministic fallback from wallet + course + const raw = (walletAddress + courseName) + .split("") + .reduce((acc, c) => ((acc << 5) + acc + c.charCodeAt(0)) | 0, 5381); + return `#${Math.abs(raw).toString(16).slice(0, 6).toUpperCase()}`; +} + +// --------------------------------------------------------------------------- +// SVG builders +// --------------------------------------------------------------------------- + +function buildCourseCompletionSVG(params: CertificateParams): string { + const { + recipientName, + walletAddress = "0x0000000000000000000000000000000000000000", + courseName = "EIPsInsight Academy Course", + courseSlug = "", + completionDate, + courseLevel = "Beginner", + serialNumber, + } = params; + + const userHue = addressToHue(walletAddress); + const accentHue = COURSE_ACCENT_HUES[courseSlug] ?? userHue; + + // Background: two dark tones based on user's unique hue + const bg1 = hsl(userHue, 28, 7); + const bg2 = hsl((userHue + 35) % 360, 22, 12); + // Accent gradient: course-specific + const accent1 = hsl(accentHue, 82, 62); + const accent2 = hsl((accentHue + 45) % 360, 76, 56); + // Text tones + const textPrimary = "#F1F5F9"; + const textSecondary = hsl(accentHue, 65, 82); + const textMuted = hsl(userHue, 18, 60); + + const serial = buildSerial(serialNumber, walletAddress, courseName); + const date = new Date(completionDate).toLocaleDateString("en-US", { + year: "numeric", month: "long", day: "numeric", + }); + const addrSnip = `${walletAddress.slice(0, 10)}…${walletAddress.slice(-6)}`; + + const safeName = xmlEscape(truncate(recipientName, 30)); + const safeCourse = xmlEscape(truncate(courseName, 44)); + const safeLevel = xmlEscape(courseLevel.toUpperCase()); + const safeDate = xmlEscape(date); + const safeSerial = xmlEscape(serial); + const safeAddr = xmlEscape(addrSnip); + + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EIPsInsight Academy + + + + + + CERTIFICATE OF COMPLETION + + + This certifies that + + + ${safeName} + + + + + + has successfully completed + + + ${safeCourse} + + + + ${safeLevel} + + + + + + Issued ${safeDate} + + + Polygon Amoy · On-Chain + + + ${safeSerial} + + + ${safeAddr} +`; +} + +function buildPioneerSVG(params: CertificateParams): string { + const { + recipientName, + walletAddress = "0x0000000000000000000000000000000000000000", + completionDate, + serialNumber, + } = params; + + const userHue = addressToHue(walletAddress); + const accentHue = PIONEER_ACCENT_HUE; // gold + + // Background: dark, unique tint from wallet + const bg1 = hsl(userHue, 25, 6); + const bg2 = hsl((userHue + 30) % 360, 20, 10); + // Gold accent + const accent1 = hsl(accentHue, 90, 58); + const accent2 = hsl((accentHue + 20) % 360, 85, 50); + // Text + const textPrimary = "#F8FAFC"; + const textSecondary = hsl(accentHue, 75, 85); + const textMuted = hsl(userHue, 16, 58); + + const serial = buildSerial(serialNumber, walletAddress, "pioneer"); + const date = new Date(completionDate).toLocaleDateString("en-US", { + year: "numeric", month: "long", day: "numeric", + }); + const addrSnip = `${walletAddress.slice(0, 10)}…${walletAddress.slice(-6)}`; + + const safeName = xmlEscape(truncate(recipientName, 30)); + const safeDate = xmlEscape(date); + const safeSerial = xmlEscape(serial); + const safeAddr = xmlEscape(addrSnip); + + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EIPsInsight Academy + + + GENESIS PIONEER AWARD + + + + + + Presented to + + + ${safeName} + + + + + + + for being an early EIPsInsight Academy Pioneer and completing the onboarding journey + + + + ★ ★ ★ + + + + + + Issued ${safeDate} + + + Polygon Amoy · On-Chain + + + ${safeSerial} + + + ${safeAddr} +`; +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Generate a unique SVG certificate as a UTF-8 Buffer. + * + * The resulting SVG can be directly uploaded to IPFS (via Pinata) as + * `image/svg+xml` and referenced in the NFT metadata `image` field. + */ +export function generateCertificateSVG(params: CertificateParams): Buffer { + const svgString = + params.type === "pioneer" + ? buildPioneerSVG(params) + : buildCourseCompletionSVG(params); + + return Buffer.from(svgString, "utf-8"); +} diff --git a/src/lib/courseData.ts b/src/lib/courseData.ts index c29d4d3..5dd2605 100644 --- a/src/lib/courseData.ts +++ b/src/lib/courseData.ts @@ -65,7 +65,7 @@ export const coursesWithPath: EnhancedCourse[] = [ { id: 'eips-101', title: 'EIPs 101: From First Principles to First Proposal', - description: 'Master Ethereum Improvement Proposals from basics to writing your first EIP using EthEd\'s tools.', + description: 'Master Ethereum Improvement Proposals from basics to writing your first EIP using EIPsInsight Academy\'s tools.', level: 'Beginner', learningPath: 'Fundamentals', duration: '2-3 hours', diff --git a/src/lib/emails/courseCompletion.ts b/src/lib/emails/courseCompletion.ts index 1f5b17d..1146921 100644 --- a/src/lib/emails/courseCompletion.ts +++ b/src/lib/emails/courseCompletion.ts @@ -15,7 +15,7 @@ export async function sendCourseCompletionEmail({ courseSlug, xpAwarded, }: CourseCompletionEmailProps) { - const siteUrl = process.env.NEXTAUTH_URL ?? "https://ethed.xyz"; + const siteUrl = process.env.NEXTAUTH_URL ?? "https://academy.eipsinsight.com"; const dashboardUrl = `${siteUrl}/dashboard`; const learnUrl = `${siteUrl}/learn/${courseSlug}`; @@ -90,7 +90,7 @@ export async function sendCourseCompletionEmail({

You’re receiving this because you completed a course on - EthEd. + EIPsInsight Academy.

@@ -101,7 +101,7 @@ export async function sendCourseCompletionEmail({ `; - const text = `Hi ${userName},\n\nCongratulations! You completed "${courseName}" and earned +${xpAwarded} XP.\n\nClaim your NFT badge and keep learning:\n${dashboardUrl}\n\n— The EthEd Team`; + const text = `Hi ${userName},\n\nCongratulations! You completed "${courseName}" and earned +${xpAwarded} XP.\n\nClaim your NFT badge and keep learning:\n${dashboardUrl}\n\n— The EIPsInsight Academy Team`; return sendMail({ to, diff --git a/src/lib/ens-service.ts b/src/lib/ens-service.ts index 2130a93..0c66fff 100644 --- a/src/lib/ens-service.ts +++ b/src/lib/ens-service.ts @@ -106,7 +106,7 @@ export function validateSubdomain(subdomain: string): { "mail", "ftp", "localhost", - "ethed", + "eipsinsight", "test", "root", "system", diff --git a/src/lib/ipfs.ts b/src/lib/ipfs.ts index 7bf3cec..6fd9aab 100644 --- a/src/lib/ipfs.ts +++ b/src/lib/ipfs.ts @@ -14,7 +14,7 @@ export function ipfsToGatewayUrl( // Developer convenience: if the repo still contains the placeholder genesis CID, // show the bundled local preview in development so the UI works without IPFS. - if (process.env.NODE_ENV !== 'production' && cidAndPath.startsWith('QmEthEdPioneer1')) { + if (process.env.NODE_ENV !== 'production' && cidAndPath.startsWith('QmEIPsInsightPioneer1')) { // Use OG image fallback in dev instead of animated GIFs return '/og-image.png'; } diff --git a/src/lib/lessonContentMap.ts b/src/lib/lessonContentMap.ts index faeafb5..7a519c3 100644 --- a/src/lib/lessonContentMap.ts +++ b/src/lib/lessonContentMap.ts @@ -332,7 +332,7 @@ EIPs can be dense and technical. Here's how to read them efficiently and extract 8: ` # Draft Your First EIP: Hands-On Workshop -Now it's time to put everything together and draft your first EIP using EthEd's Proposal Builder. +Now it's time to put everything together and draft your first EIP using EIPsInsight Academy's Proposal Builder. ## Step 1: Problem Identification (10 minutes) diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 7d3a05b..629f371 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -1,5 +1,5 @@ /** - * Centralized metrics configuration for eth.ed platform + * Centralized metrics configuration for EIPsInsight Academy platform * Single source of truth for all platform statistics */ diff --git a/src/lib/nft-service.ts b/src/lib/nft-service.ts index c416e65..8e9e1ff 100644 --- a/src/lib/nft-service.ts +++ b/src/lib/nft-service.ts @@ -6,7 +6,8 @@ import { pinFile, pinJSON } from "./pinata-config"; import { prisma } from "@/lib/prisma-client"; import type { Prisma } from "@prisma/client"; -import { GENESIS_PIONEER_IMAGE_URI, GENESIS_PIONEER_METADATA_URI } from "@/lib/genesis-assets"; +import { GENESIS_PIONEER_METADATA_URI } from "@/lib/genesis-assets"; +import { generateCertificateSVG } from "@/lib/certificate-generator"; import { getContractAddress, AMOY_CHAIN_ID, @@ -17,8 +18,10 @@ import { getDeployerAddress, getPublicClient, getWalletClient, + getDeployerAccount, isOnChainEnabled, } from "@/lib/viem-client"; +import { encodeFunctionData } from "viem"; import { logger } from "@/lib/monitoring"; import fs from "fs"; import path from "path"; @@ -127,8 +130,8 @@ export function generateGenesisScholarMetadata( ensName?: string ): NFTMetadata { return { - name: ensName ? `eth.ed Pioneer - ${ensName}` : "eth.ed Pioneer NFT", - description: `Commemorates ${ensName || 'a dedicated scholar'} being an early eth.ed pioneer and completing the onboarding journey.`, + name: ensName ? `EIPsInsight Academy Pioneer - ${ensName}` : "EIPsInsight Academy Pioneer NFT", + description: `Commemorates ${ensName || 'a dedicated scholar'} being an early EIPsInsight Academy pioneer and completing the onboarding journey.`, image: imageUri, attributes: [ { trait_type: "Type", value: "Genesis Scholar" }, @@ -137,7 +140,7 @@ export function generateGenesisScholarMetadata( { trait_type: "Minted Date", value: new Date().toISOString().split("T")[0] }, ...(ensName ? [{ trait_type: "ENS Name", value: ensName }] : []), ], - external_url: "https://ethed.app", + external_url: "https://academy.eipsinsight.com", }; } @@ -159,11 +162,17 @@ export async function mintNFTAndSave( throw new Error("User not found"); } - // Generate metadata - const metadata = generateGenesisScholarMetadata( - GENESIS_PIONEER_IMAGE_URI, - ensName - ); + // Generate a unique SVG Pioneer certificate for this recipient + const certSvg = generateCertificateSVG({ + type: "pioneer", + recipientName: ensName || user.name || "Pioneer", + walletAddress: userAddress, + completionDate: new Date().toISOString(), + }); + const imageUri = await uploadCertificateToIPFS(certSvg, `pioneer-${userId}-${Date.now()}.svg`); + + // Generate metadata referencing the unique certificate image + const metadata = generateGenesisScholarMetadata(imageUri, ensName); // Upload metadata to IPFS const metadataUri = await uploadMetadataToIPFS(metadata); @@ -181,14 +190,16 @@ export async function mintNFTAndSave( } else { // Off-chain record only (no wallet connected or on-chain disabled) tokenId = `off-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + txHash = null; // no real transaction + contractAddr = null; } // Save NFT record to database const nft = await prisma.nFT.create({ data: { userId, - name: `eth.ed Genesis Pioneer - ${ensName || user.name || "Scholar"}`, - image: GENESIS_PIONEER_IMAGE_URI, + name: `EIPsInsight Academy Genesis Pioneer - ${ensName || user.name || "Scholar"}`, + image: imageUri, metadata: metadataUri, contractAddress: contractAddr, tokenId, @@ -238,24 +249,60 @@ export async function mintOnChain( logger.warn("On-chain minting disabled (missing env vars) — using dev mock", "nft-service"); await new Promise((resolve) => setTimeout(resolve, 500)); const mockTokenId = `mock-${Date.now()}-${Math.random().toString(36).substring(7)}`; - const mockTxHash = `0x${"0".repeat(64)}`; - return { tokenId: mockTokenId, txHash: mockTxHash, contractAddress }; + // Return empty txHash so callers know this wasn't a real on-chain mint + return { tokenId: mockTokenId, txHash: "", contractAddress }; } const publicClient = getPublicClient(); const walletClient = getWalletClient(); + const deployerAccount = getDeployerAccount(); logger.info(`Minting NFT to ${recipientAddress}`, "nft-service", { metadataUri }); try { - // Send the mint transaction via the server relayer wallet - const txHash = await walletClient.writeContract({ - address: contractAddress, + logger.info(`Using contract address: ${contractAddress}`, "nft-service"); + logger.info(`Deployer address: ${deployerAccount.address}`, "nft-service"); + + // Encode the contract call data + const encodedData = encodeFunctionData({ abi: NFT_CONTRACT_ABI, functionName: "mint", args: [recipientAddress as `0x${string}`, metadataUri], - account: getDeployerAddress(), - chain: undefined, + }); + + // Get current nonce + const nonce = await publicClient.getTransactionCount({ + address: deployerAccount.address, + }); + + // Estimate gas + const gasEstimate = await publicClient.estimateGas({ + account: deployerAccount, + to: contractAddress, + data: encodedData, + }); + + // Get gas price + const gasPrice = await publicClient.getGasPrice(); + + // Build the transaction + const tx = { + to: contractAddress as `0x${string}`, + data: encodedData, + nonce, + gasPrice, + gas: gasEstimate + BigInt(10000), // Add small buffer + chainId: AMOY_CHAIN_ID, + }; + + logger.info(`Built transaction`, "nft-service", { nonce, gasPrice: gasPrice.toString(), gas: (gasEstimate + BigInt(10000)).toString() }); + + // Sign the transaction + const serialized = await walletClient.signTransaction(tx); + + // Send the raw transaction + const txHash = await publicClient.sendRawTransaction({ + serializedTransaction: serialized, }); logger.info(`Mint tx sent: ${txHash}`, "nft-service"); @@ -298,11 +345,25 @@ export async function mintOnChain( logger.error( "On-chain mint failed", "nft-service", - { recipientAddress, metadataUri }, + { recipientAddress, metadataUri, contractAddress, deployerAddress: deployerAccount.address }, error ); + + // Get more detailed error info + let errorMessage = "Unknown error"; + if (error instanceof Error) { + errorMessage = error.message; + // If it's a Viem error with details, include them + if ((error as any).details) { + errorMessage += ` | Details: ${(error as any).details}`; + } + if ((error as any).code) { + errorMessage += ` | Code: ${(error as any).code}`; + } + } + throw new Error( - `On-chain mint failed: ${error instanceof Error ? error.message : "Unknown error"}` + `On-chain mint failed: ${errorMessage}` ); } } @@ -343,21 +404,18 @@ export async function saveNFTToDatabase(params: { export async function mintGenesisNFTs(params: MintNFTParams) { const { userId, ensName, userAddress } = params; - // Development-friendly fallback: if the genesis image is still the placeholder - // and Pinata is not configured, use a bundled local image so the UI works offline. - const placeholderCid = "ipfs://QmEthEdPioneer1" as string; - // Use the Learning Sprout image as the default for pioneers - const devLocalImage = "/nft-learning-sprout.png"; - const genesisImageUri = - GENESIS_PIONEER_IMAGE_URI === placeholderCid && !env.PINATA_JWT - ? devLocalImage - : GENESIS_PIONEER_IMAGE_URI; - - // Generate metadata - const genesisMetadata = generateGenesisScholarMetadata( - genesisImageUri, - ensName - ); + // Generate a unique SVG Pioneer certificate for this recipient + const pioneerSvgBuffer = generateCertificateSVG({ + type: "pioneer", + recipientName: ensName || "Pioneer", + walletAddress: userAddress, + completionDate: new Date().toISOString(), + }); + const pioneerFilename = `pioneer-${Date.now()}.svg`; + const genesisImageUri = await uploadCertificateToIPFS(pioneerSvgBuffer, pioneerFilename); + + // Generate metadata (reference unique SVG) + const genesisMetadata = generateGenesisScholarMetadata(genesisImageUri, ensName); const genesisMetadataUri = GENESIS_PIONEER_METADATA_URI ? GENESIS_PIONEER_METADATA_URI @@ -372,25 +430,25 @@ export async function mintGenesisNFTs(params: MintNFTParams) { // Off-chain record only (no wallet connected or on-chain disabled) genesisResult = { tokenId: `off-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - txHash: `0x${"0".repeat(64)}`, - contractAddress: getContractAddress(AMOY_CHAIN_ID, "NFT_CONTRACT"), + txHash: "", + contractAddress: "", }; } - // Save to database with on-chain data + // Save to database with on-chain data (null for off-chain mints) const genesisNFT = await saveNFTToDatabase({ userId, tokenId: genesisResult.tokenId, - name: "eth.ed Pioneer NFT", + name: "EIPsInsight Academy Pioneer NFT", image: genesisImageUri, metadata: genesisMetadata, - contractAddress: genesisResult.contractAddress, - transactionHash: genesisResult.txHash, + contractAddress: genesisResult.contractAddress || undefined, + transactionHash: genesisResult.txHash || undefined, ownerAddress: userAddress || undefined, chainId: AMOY_CHAIN_ID, }); - const explorerUrl = genesisResult.txHash && !genesisResult.txHash.startsWith("0x" + "0".repeat(64)) + const explorerUrl = genesisResult.txHash && genesisResult.txHash.length > 2 ? getExplorerTxUrl(AMOY_CHAIN_ID, genesisResult.txHash) : null; @@ -408,34 +466,51 @@ export async function mintGenesisNFTs(params: MintNFTParams) { } /** - * Upload course completion NFT image (Learning Sprout GIF) to IPFS + * Upload a generated SVG certificate Buffer to IPFS via Pinata. + * + * Falls back to a local file path in development when Pinata is not configured, + * exactly like the existing metadata upload path. */ -export async function uploadCourseSproutToIPFS(): Promise { - try { - // Read the sprout GIF from public folder - const sproutPath = path.join(process.cwd(), "public", "nft-learning-sprout.gif"); - const imageBuffer = fs.readFileSync(sproutPath); - - // If Pinata not configured, return OG PNG (we don't need the course GIFs right now) - if (!env.PINATA_JWT) { - logger.warn("Pinata JWT not configured — using OG PNG at /og-image.png", "nft-service"); - return "/og-image.png"; +export async function uploadCertificateToIPFS( + svgBuffer: Buffer, + filename: string +): Promise { + if (!env.PINATA_JWT) { + if (env.NODE_ENV === "production") { + throw new Error("Pinata not configured — PINATA_JWT is required in production"); } + // Dev fallback: persist SVG alongside local metadata + try { + const outDir = path.join(process.cwd(), "public", "local-metadata"); + if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); + const outPath = path.join(outDir, filename); + fs.writeFileSync(outPath, svgBuffer); + logger.warn(`Saved certificate SVG to /local-metadata/${filename} (Pinata not configured)`, "nft-service"); + return `/local-metadata/${filename}`; + } catch { + throw new Error("Failed to write local certificate fallback"); + } + } - // Convert to File for Pinata - const arrayBuffer = imageBuffer.buffer.slice( - imageBuffer.byteOffset, - imageBuffer.byteOffset + imageBuffer.byteLength - ) as ArrayBuffer; - const blob = new Blob([arrayBuffer], { type: "image/gif" }); - const file = new File([blob], "learning-sprout.gif", { type: "image/gif" }); - + try { + const file = new File([new Uint8Array(svgBuffer)], filename, { type: "image/svg+xml" }); return await pinFile(file); } catch (error: unknown) { - // Fallback to OG PNG if IPFS fails in dev const msg = error instanceof Error ? error.message : String(error); - logger.warn(`uploadCourseSproutToIPFS failed, falling back to OG PNG: ${msg}`, "nft-service"); - return "/og-image.png"; + if (env.NODE_ENV !== "production") { + // Dev: fall back to local file + try { + const outDir = path.join(process.cwd(), "public", "local-metadata"); + if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); + const outPath = path.join(outDir, filename); + fs.writeFileSync(outPath, svgBuffer); + logger.warn(`Pinata upload failed, saved certificate to /local-metadata/${filename}: ${msg}`, "nft-service"); + return `/local-metadata/${filename}`; + } catch { + // fall through + } + } + throw new Error(`Failed to upload certificate to IPFS: ${msg}`); } } @@ -446,25 +521,27 @@ export function generateCourseCompletionMetadata( imageUri: string, courseName: string, courseSlug: string, - recipientName?: string + recipientName?: string, + courseLevel?: string ): NFTMetadata { + const recipient = recipientName || "Scholar"; return { - name: `${courseName}${recipientName ? ` - ${recipientName}` : ''} - Learning Sprout`, - description: `Commemorates the successful completion of ${courseName} on eth.ed by ${recipientName || 'a dedicated scholar'}. This Learning Sprout represents your growth and mastery in blockchain education.`, + name: `${courseName} — ${recipient}`, + description: `On-chain certificate of completion for ${courseName} on EIPsInsight Academy, awarded to ${recipient}. This unique certificate is generated specifically for this recipient and is permanently recorded on the blockchain.`, image: imageUri, - animation_url: imageUri, // GIF works as animation - courseSlug, // Used for UI linkage In Profile - courseName, // Used for UI linkage In Profile + courseSlug, + courseName, attributes: [ { trait_type: "Type", value: "Course Completion" }, { trait_type: "Course", value: courseName }, { trait_type: "Course Slug", value: courseSlug }, - { trait_type: "Recipient", value: recipientName || "Scholar" }, - { trait_type: "Platform", value: "eth.ed" }, + { trait_type: "Recipient", value: recipient }, + { trait_type: "Platform", value: "EIPsInsight Academy" }, + { trait_type: "Level", value: courseLevel || "Beginner" }, { trait_type: "Completion Date", value: new Date().toISOString().split("T")[0] }, - { trait_type: "NFT Design", value: "Learning Sprout" } + { trait_type: "Certificate Type", value: "Unique On-Chain SVG" }, ], - external_url: `https://ethed.app/courses/${courseSlug}`, + external_url: `https://academy.eipsinsight.com/courses/${courseSlug}`, }; } @@ -477,14 +554,29 @@ export async function mintCourseCompletionNFT(params: { courseName: string; userAddress?: string; recipientName?: string; + courseLevel?: string; }) { - const { userId, courseSlug, courseName, userAddress, recipientName } = params; + const { userId, courseSlug, courseName, userAddress, recipientName, courseLevel } = params; + + // --- Generate a unique SVG certificate for this recipient --- + const certSvgBuffer = generateCertificateSVG({ + type: "course-completion", + recipientName: recipientName || "Scholar", + walletAddress: userAddress, + courseName, + courseSlug, + completionDate: new Date().toISOString(), + courseLevel, + }); - // Upload sprout GIF to IPFS - const imageUri = await uploadCourseSproutToIPFS(); + // Upload SVG certificate image to IPFS + const certFilename = `cert-${courseSlug}-${Date.now()}.svg`; + const imageUri = await uploadCertificateToIPFS(certSvgBuffer, certFilename); - // Generate metadata - const metadata = generateCourseCompletionMetadata(imageUri, courseName, courseSlug, recipientName); + // Generate metadata (now references the unique SVG image) + const metadata = generateCourseCompletionMetadata( + imageUri, courseName, courseSlug, recipientName, courseLevel + ); // Upload metadata to IPFS const metadataUri = await uploadMetadataToIPFS(metadata); @@ -495,36 +587,50 @@ export async function mintCourseCompletionNFT(params: { if (userAddress && isOnChainEnabled()) { mintResult = await mintOnChain(userAddress, metadataUri, "course-completion"); } else { - // Off-chain record only (no wallet connected or on-chain disabled) mintResult = { tokenId: `off-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - txHash: `0x${"0".repeat(64)}`, - contractAddress: getContractAddress(AMOY_CHAIN_ID, "NFT_CONTRACT"), + txHash: "", + contractAddress: "", }; } - // Save to database with on-chain data + // Update SVG with minted tokenId so the serial number is accurate + const finalCertBuffer = generateCertificateSVG({ + type: "course-completion", + recipientName: recipientName || "Scholar", + walletAddress: userAddress, + courseName, + courseSlug, + completionDate: new Date().toISOString(), + courseLevel, + serialNumber: mintResult.tokenId, + }); + const finalFilename = `cert-${courseSlug}-${mintResult.tokenId.slice(0, 12)}.svg`; + const finalImageUri = await uploadCertificateToIPFS(finalCertBuffer, finalFilename).catch(() => imageUri); + + // Save to database (null for off-chain mints so dashboard won't show invalid explorer links) const nft = await saveNFTToDatabase({ userId, tokenId: mintResult.tokenId, - name: `${courseName}${recipientName ? ` - ${recipientName}` : ''} - Learning Sprout`, - image: imageUri, - metadata, - contractAddress: mintResult.contractAddress, - transactionHash: mintResult.txHash, + name: `${courseName} — ${recipientName || "Scholar"}`, + image: finalImageUri, + metadata: { ...metadata, image: finalImageUri }, + contractAddress: mintResult.contractAddress || undefined, + transactionHash: mintResult.txHash || undefined, ownerAddress: userAddress || undefined, chainId: AMOY_CHAIN_ID, }); - const explorerUrl = mintResult.txHash && !mintResult.txHash.startsWith("0x" + "0".repeat(64)) - ? getExplorerTxUrl(AMOY_CHAIN_ID, mintResult.txHash) - : null; + const explorerUrl = + mintResult.txHash && mintResult.txHash.length > 2 + ? getExplorerTxUrl(AMOY_CHAIN_ID, mintResult.txHash) + : null; return { nft, - transaction: { - type: "course-completion", - txHash: mintResult.txHash, + transaction: { + type: "course-completion", + txHash: mintResult.txHash, tokenId: mintResult.tokenId, courseSlug, courseName, @@ -626,7 +732,7 @@ export async function syncUserNFTs(userId: string) { data: { userId, tokenId, - name: metadata.name || `EthEd Certificate #${tokenId}`, + name: metadata.name || `EIPsInsight Academy Certificate #${tokenId}`, image: metadata.image || "", metadata: metadata as unknown as Prisma.InputJsonValue, contractAddress, diff --git a/src/lib/prisma-client.ts b/src/lib/prisma-client.ts index 4c5e2a1..68da7c9 100644 --- a/src/lib/prisma-client.ts +++ b/src/lib/prisma-client.ts @@ -11,7 +11,7 @@ function validateDatabaseUrl() { if (!dbUrl) { throw new Error( - "Missing DATABASE_URL environment variable. Set DATABASE_URL in your .env.local (e.g. DATABASE_URL=postgresql://user:password@localhost:5432/ethed) and restart the dev server." + "Missing DATABASE_URL environment variable. Set DATABASE_URL in your .env.local (e.g. DATABASE_URL=postgresql://user:password@localhost:5432/eipsinsight) and restart the dev server." ); } @@ -19,20 +19,53 @@ function validateDatabaseUrl() { const normalized = dbUrl.trim().replace(/^['"]|['"]$/g, ""); if (!/^postgres(?:ql)?:\/\//i.test(normalized)) { throw new Error( - `Invalid DATABASE_URL: '${normalized}'. Prisma expects a Postgres connection string starting with 'postgresql://' or 'postgres://'. Example: 'postgresql://user:password@localhost:5432/ethed'` + `Invalid DATABASE_URL: '${normalized}'. Prisma expects a Postgres connection string starting with 'postgresql://' or 'postgres://'. Example: 'postgresql://user:password@localhost:5432/eipsinsight'` ); } } validateDatabaseUrl(); -const dbUrl = process.env.DATABASE_URL?.trim().replace(/^['"]|['"]$/g, ""); +/** + * Append PgBouncer-compatible parameters to the connection URL so that Prisma + * does NOT use named prepared statements. Without this, concurrent serverless + * invocations (Vercel / Supabase) hit PostgreSQL error 42P05 + * ("prepared statement already exists"). + */ +function buildConnectionUrl(raw: string | undefined): string | undefined { + if (!raw) return raw; + const cleaned = raw.trim().replace(/^['"']|['"']$/g, ""); + try { + const url = new URL(cleaned); + if (!url.searchParams.has("pgbouncer")) { + url.searchParams.set("pgbouncer", "true"); + } + // In dev, allow more concurrent connections to avoid request serialization. + // In production (serverless), keep it at 1 per invocation. + if (!url.searchParams.has("connection_limit")) { + url.searchParams.set("connection_limit", process.env.NODE_ENV === "development" ? "5" : "1"); + } + // Fail fast if Supabase pooler is slow to accept (e.g. cold-start). + if (!url.searchParams.has("connect_timeout")) { + url.searchParams.set("connect_timeout", "15"); + } + // Don't wait forever for a pooled connection slot. + if (!url.searchParams.has("pool_timeout")) { + url.searchParams.set("pool_timeout", "15"); + } + return url.toString(); + } catch { + return cleaned; + } +} + +const dbUrl = buildConnectionUrl(process.env.DATABASE_URL); const prismaClientSingleton = () => { return new PrismaClient({ datasources: { db: { - url: dbUrl, + url: dbUrl, // includes ?pgbouncer=true&connection_limit=1 }, }, log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], diff --git a/src/lib/viem-client.ts b/src/lib/viem-client.ts index 2b45a47..581e3e4 100644 --- a/src/lib/viem-client.ts +++ b/src/lib/viem-client.ts @@ -34,7 +34,7 @@ function getRpcUrl(): string { return url; } -function getDeployerAccount(): Account { +export function getDeployerAccount(): Account { const key = process.env.DEPLOYER_PRIVATE_KEY; if (!key) { throw new Error( diff --git a/src/proxy.ts b/src/proxy.ts index d7f2c70..809c2ac 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -3,7 +3,7 @@ import type { NextRequest } from 'next/server'; import { getToken } from 'next-auth/jwt'; /** - * Security proxy for the EthEd platform + * Security proxy for the EIPsInsight Academy platform * Adds security headers and handles route protection */ export async function proxy(request: NextRequest) { From 48dc210fedfe1a33a071e5ab14c7f3e103ce52df Mon Sep 17 00:00:00 2001 From: Ayush Date: Sat, 7 Mar 2026 02:27:44 +0530 Subject: [PATCH 2/4] fix: change toast.warn to toast.warning for better clarity in wallet connection message --- src/components/EnhancedLessonViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/EnhancedLessonViewer.tsx b/src/components/EnhancedLessonViewer.tsx index 8ce5558..b4bcfa3 100644 --- a/src/components/EnhancedLessonViewer.tsx +++ b/src/components/EnhancedLessonViewer.tsx @@ -248,7 +248,7 @@ export default function EnhancedLessonViewer({ // Sync final progress to the backend before minting if (!session?.address) { // let user know that without a connected wallet the NFT will be off-chain - toast.warn( + toast.warning( "No wallet connected — NFT will be recorded off-chain.", { description: "Connect a wallet if you want the certificate minted on Polygon." } ); From a7e00d4dd775285eb4af92556952e0f5b35fd0cc Mon Sep 17 00:00:00 2001 From: Ayush Date: Sat, 7 Mar 2026 02:31:08 +0530 Subject: [PATCH 3/4] feat: add deployerAccount to transaction object in mintOnChain function --- src/lib/nft-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/nft-service.ts b/src/lib/nft-service.ts index 8e9e1ff..562ee46 100644 --- a/src/lib/nft-service.ts +++ b/src/lib/nft-service.ts @@ -287,6 +287,7 @@ export async function mintOnChain( // Build the transaction const tx = { + account: deployerAccount, to: contractAddress as `0x${string}`, data: encodedData, nonce, From 5d5ec8c784aae5c4ae4ca93574af70d380d1776d Mon Sep 17 00:00:00 2001 From: Ayush Date: Sat, 7 Mar 2026 02:39:04 +0530 Subject: [PATCH 4/4] fix: specify chain explicitly in transaction signing for type safety --- src/lib/nft-service.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/nft-service.ts b/src/lib/nft-service.ts index 562ee46..b1d233d 100644 --- a/src/lib/nft-service.ts +++ b/src/lib/nft-service.ts @@ -22,6 +22,7 @@ import { isOnChainEnabled, } from "@/lib/viem-client"; import { encodeFunctionData } from "viem"; +import { polygonAmoy } from "viem/chains"; import { logger } from "@/lib/monitoring"; import fs from "fs"; import path from "path"; @@ -298,8 +299,11 @@ export async function mintOnChain( logger.info(`Built transaction`, "nft-service", { nonce, gasPrice: gasPrice.toString(), gas: (gasEstimate + BigInt(10000)).toString() }); - // Sign the transaction - const serialized = await walletClient.signTransaction(tx); + // Sign the transaction (specify chain explicitly for type safety) + const serialized = await walletClient.signTransaction({ + ...tx, + chain: polygonAmoy, + }); // Send the raw transaction const txHash = await publicClient.sendRawTransaction({