Skip to content

Commit 6c6d3c2

Browse files
authored
Merge branch 'main' into generative-ai-data-use
2 parents f7aaa1b + fd228fe commit 6c6d3c2

21 files changed

Lines changed: 592 additions & 36 deletions

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Description
2+
3+
<!--
4+
5+
- Summarize your changes and explain the rationale for making them.
6+
- Include any relevant context from Slack, meetings, or other discussions.
7+
- If this change is resolving a specific issue, include a link to the issue (e.g. "closes #1234").
8+
- If applicable, include screenshots and/or videos.
9+
- Note the level of AI assistance used (i.e. none, specific portions, majority of implementation).
10+
11+
-->
12+
13+
# Testing
14+
15+
<!--
16+
17+
- Share how you tested your changes.
18+
- If you're fixing a bug, explain how to reproduce the original issue.
19+
- If applicable, make sure you've authored unit and/or integration tests.
20+
- If applicable, provide instructions for a reviewer to test the changes for themselves.
21+
- If applicable, mention any accessibility testing that was done.
22+
23+
If you re-test your PR after significant changes, please add a comment to the PR saying so.
24+
25+
Self-reviews with GitHub's review UI are encouraged to point out the following:
26+
27+
- Explanations for code or design decisions that may confuse reviewers.
28+
- Particularly important or core changes.
29+
- Items that may need further discussion.
30+
- Changes that may warrant additional testing.
31+
32+
-->

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules/
22
.next/
3+
.yarn/

.yarn/releases/yarn-4.9.2.cjs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
#!/usr/bin/env node
2-
/* eslint-disable */
3-
//prettier-ignore
42
(()=>{var UVe=Object.create;var E_=Object.defineProperty;var HVe=Object.getOwnPropertyDescriptor;var jVe=Object.getOwnPropertyNames;var qVe=Object.getPrototypeOf,GVe=Object.prototype.hasOwnProperty;var Ie=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,r)=>(typeof require<"u"?require:e)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var Ct=(t,e)=>()=>(t&&(e=t(t=0)),e);var L=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),Vt=(t,e)=>{for(var r in e)E_(t,r,{get:e[r],enumerable:!0})},WVe=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of jVe(e))!GVe.call(t,a)&&a!==r&&E_(t,a,{get:()=>e[a],enumerable:!(s=HVe(e,a))||s.enumerable});return t};var et=(t,e,r)=>(r=t!=null?UVe(qVe(t)):{},WVe(e||!t||!t.__esModule?E_(r,"default",{value:t,enumerable:!0}):r,t));var fi={};Vt(fi,{SAFE_TIME:()=>d$,S_IFDIR:()=>rx,S_IFLNK:()=>nx,S_IFMT:()=>_f,S_IFREG:()=>N2});var _f,rx,N2,nx,d$,m$=Ct(()=>{_f=61440,rx=16384,N2=32768,nx=40960,d$=456789e3});var or={};Vt(or,{EBADF:()=>Uo,EBUSY:()=>YVe,EEXIST:()=>XVe,EINVAL:()=>KVe,EISDIR:()=>ZVe,ENOENT:()=>JVe,ENOSYS:()=>VVe,ENOTDIR:()=>zVe,ENOTEMPTY:()=>e7e,EOPNOTSUPP:()=>t7e,EROFS:()=>$Ve,ERR_DIR_CLOSED:()=>I_});function wc(t,e){return Object.assign(new Error(`${t}: ${e}`),{code:t})}function YVe(t){return wc("EBUSY",t)}function VVe(t,e){return wc("ENOSYS",`${t}, ${e}`)}function KVe(t){return wc("EINVAL",`invalid argument, ${t}`)}function Uo(t){return wc("EBADF",`bad file descriptor, ${t}`)}function JVe(t){return wc("ENOENT",`no such file or directory, ${t}`)}function zVe(t){return wc("ENOTDIR",`not a directory, ${t}`)}function ZVe(t){return wc("EISDIR",`illegal operation on a directory, ${t}`)}function XVe(t){return wc("EEXIST",`file already exists, ${t}`)}function $Ve(t){return wc("EROFS",`read-only filesystem, ${t}`)}function e7e(t){return wc("ENOTEMPTY",`directory not empty, ${t}`)}function t7e(t){return wc("EOPNOTSUPP",`operation not supported, ${t}`)}function I_(){return wc("ERR_DIR_CLOSED","Directory handle was closed")}var ix=Ct(()=>{});var el={};Vt(el,{BigIntStatsEntry:()=>rE,DEFAULT_MODE:()=>B_,DirEntry:()=>C_,StatEntry:()=>tE,areStatsEqual:()=>v_,clearStats:()=>sx,convertToBigIntStats:()=>n7e,makeDefaultStats:()=>y$,makeEmptyStats:()=>r7e});function y$(){return new tE}function r7e(){return sx(y$())}function sx(t){for(let e in t)if(Object.hasOwn(t,e)){let r=t[e];typeof r=="number"?t[e]=0:typeof r=="bigint"?t[e]=BigInt(0):w_.types.isDate(r)&&(t[e]=new Date(0))}return t}function n7e(t){let e=new rE;for(let r in t)if(Object.hasOwn(t,r)){let s=t[r];typeof s=="number"?e[r]=BigInt(s):w_.types.isDate(s)&&(e[r]=new Date(s))}return e.atimeNs=e.atimeMs*BigInt(1e6),e.mtimeNs=e.mtimeMs*BigInt(1e6),e.ctimeNs=e.ctimeMs*BigInt(1e6),e.birthtimeNs=e.birthtimeMs*BigInt(1e6),e}function v_(t,e){if(t.atimeMs!==e.atimeMs||t.birthtimeMs!==e.birthtimeMs||t.blksize!==e.blksize||t.blocks!==e.blocks||t.ctimeMs!==e.ctimeMs||t.dev!==e.dev||t.gid!==e.gid||t.ino!==e.ino||t.isBlockDevice()!==e.isBlockDevice()||t.isCharacterDevice()!==e.isCharacterDevice()||t.isDirectory()!==e.isDirectory()||t.isFIFO()!==e.isFIFO()||t.isFile()!==e.isFile()||t.isSocket()!==e.isSocket()||t.isSymbolicLink()!==e.isSymbolicLink()||t.mode!==e.mode||t.mtimeMs!==e.mtimeMs||t.nlink!==e.nlink||t.rdev!==e.rdev||t.size!==e.size||t.uid!==e.uid)return!1;let r=t,s=e;return!(r.atimeNs!==s.atimeNs||r.mtimeNs!==s.mtimeNs||r.ctimeNs!==s.ctimeNs||r.birthtimeNs!==s.birthtimeNs)}var w_,B_,C_,tE,rE,S_=Ct(()=>{w_=et(Ie("util")),B_=33188,C_=class{constructor(){this.name="";this.path="";this.mode=0}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&61440)===16384}isFIFO(){return!1}isFile(){return(this.mode&61440)===32768}isSocket(){return!1}isSymbolicLink(){return(this.mode&61440)===40960}},tE=class{constructor(){this.uid=0;this.gid=0;this.size=0;this.blksize=0;this.atimeMs=0;this.mtimeMs=0;this.ctimeMs=0;this.birthtimeMs=0;this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=0;this.ino=0;this.mode=B_;this.nlink=1;this.rdev=0;this.blocks=1}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&61440)===16384}isFIFO(){return!1}isFile(){return(this.mode&61440)===32768}isSocket(){return!1}isSymbolicLink(){return(this.mode&61440)===40960}},rE=class{constructor(){this.uid=BigInt(0);this.gid=BigInt(0);this.size=BigInt(0);this.blksize=BigInt(0);this.atimeMs=BigInt(0);this.mtimeMs=BigInt(0);this.ctimeMs=BigInt(0);this.birthtimeMs=BigInt(0);this.atimeNs=BigInt(0);this.mtimeNs=BigInt(0);this.ctimeNs=BigInt(0);this.birthtimeNs=BigInt(0);this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=BigInt(0);this.ino=BigInt(0);this.mode=BigInt(B_);this.nlink=BigInt(1);this.rdev=BigInt(0);this.blocks=BigInt(1)}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&BigInt(61440))===BigInt(16384)}isFIFO(){return!1}isFile(){return(this.mode&BigInt(61440))===BigInt(32768)}isSocket(){return!1}isSymbolicLink(){return(this.mode&BigInt(61440))===BigInt(40960)}}});function l7e(t){let e,r;if(e=t.match(o7e))t=e[1];else if(r=t.match(a7e))t=`\\\\${r[1]?".\\":""}${r[2]}`;else return t;return t.replace(/\//g,"\\")}function c7e(t){t=t.replace(/\\/g,"/");let e,r;return(e=t.match(i7e))?t=`/${e[1]}`:(r=t.match(s7e))&&(t=`/unc/${r[1]?".dot/":""}${r[2]}`),t}function ox(t,e){return t===ue?I$(e):D_(e)}var O2,vt,Er,ue,K,E$,i7e,s7e,o7e,a7e,D_,I$,tl=Ct(()=>{O2=et(Ie("path")),vt={root:"/",dot:".",parent:".."},Er={home:"~",nodeModules:"node_modules",manifest:"package.json",lockfile:"yarn.lock",virtual:"__virtual__",pnpJs:".pnp.js",pnpCjs:".pnp.cjs",pnpData:".pnp.data.json",pnpEsmLoader:".pnp.loader.mjs",rc:".yarnrc.yml",env:".env"},ue=Object.create(O2.default),K=Object.create(O2.default.posix);ue.cwd=()=>process.cwd();K.cwd=process.platform==="win32"?()=>D_(process.cwd()):process.cwd;process.platform==="win32"&&(K.resolve=(...t)=>t.length>0&&K.isAbsolute(t[0])?O2.default.posix.resolve(...t):O2.default.posix.resolve(K.cwd(),...t));E$=function(t,e,r){return e=t.normalize(e),r=t.normalize(r),e===r?".":(e.endsWith(t.sep)||(e=e+t.sep),r.startsWith(e)?r.slice(e.length):null)};ue.contains=(t,e)=>E$(ue,t,e);K.contains=(t,e)=>E$(K,t,e);i7e=/^([a-zA-Z]:.*)$/,s7e=/^\/\/(\.\/)?(.*)$/,o7e=/^\/([a-zA-Z]:.*)$/,a7e=/^\/unc\/(\.dot\/)?(.*)$/;D_=process.platform==="win32"?c7e:t=>t,I$=process.platform==="win32"?l7e:t=>t;ue.fromPortablePath=I$;ue.toPortablePath=D_});async function ax(t,e){let r="0123456789abcdef";await t.mkdirPromise(e.indexPath,{recursive:!0});let s=[];for(let a of r)for(let n of r)s.push(t.mkdirPromise(t.pathUtils.join(e.indexPath,`${a}${n}`),{recursive:!0}));return await Promise.all(s),e.indexPath}async function C$(t,e,r,s,a){let n=t.pathUtils.normalize(e),c=r.pathUtils.normalize(s),f=[],p=[],{atime:h,mtime:E}=a.stableTime?{atime:gd,mtime:gd}:await r.lstatPromise(c);await t.mkdirpPromise(t.pathUtils.dirname(e),{utimes:[h,E]}),await b_(f,p,t,n,r,c,{...a,didParentExist:!0});for(let C of f)await C();await Promise.all(p.map(C=>C()))}async function b_(t,e,r,s,a,n,c){let f=c.didParentExist?await w$(r,s):null,p=await a.lstatPromise(n),{atime:h,mtime:E}=c.stableTime?{atime:gd,mtime:gd}:p,C;switch(!0){case p.isDirectory():C=await f7e(t,e,r,s,f,a,n,p,c);break;case p.isFile():C=await h7e(t,e,r,s,f,a,n,p,c);break;case p.isSymbolicLink():C=await g7e(t,e,r,s,f,a,n,p,c);break;default:throw new Error(`Unsupported file type (${p.mode})`)}return(c.linkStrategy?.type!=="HardlinkFromIndex"||!p.isFile())&&((C||f?.mtime?.getTime()!==E.getTime()||f?.atime?.getTime()!==h.getTime())&&(e.push(()=>r.lutimesPromise(s,h,E)),C=!0),(f===null||(f.mode&511)!==(p.mode&511))&&(e.push(()=>r.chmodPromise(s,p.mode&511)),C=!0)),C}async function w$(t,e){try{return await t.lstatPromise(e)}catch{return null}}async function f7e(t,e,r,s,a,n,c,f,p){if(a!==null&&!a.isDirectory())if(p.overwrite)t.push(async()=>r.removePromise(s)),a=null;else return!1;let h=!1;a===null&&(t.push(async()=>{try{await r.mkdirPromise(s,{mode:f.mode})}catch(S){if(S.code!=="EEXIST")throw S}}),h=!0);let E=await n.readdirPromise(c),C=p.didParentExist&&!a?{...p,didParentExist:!1}:p;if(p.stableSort)for(let S of E.sort())await b_(t,e,r,r.pathUtils.join(s,S),n,n.pathUtils.join(c,S),C)&&(h=!0);else(await Promise.all(E.map(async P=>{await b_(t,e,r,r.pathUtils.join(s,P),n,n.pathUtils.join(c,P),C)}))).some(P=>P)&&(h=!0);return h}async function A7e(t,e,r,s,a,n,c,f,p,h){let E=await n.checksumFilePromise(c,{algorithm:"sha1"}),C=420,S=f.mode&511,P=`${E}${S!==C?S.toString(8):""}`,I=r.pathUtils.join(h.indexPath,E.slice(0,2),`${P}.dat`),R;(ce=>(ce[ce.Lock=0]="Lock",ce[ce.Rename=1]="Rename"))(R||={});let N=1,U=await w$(r,I);if(a){let ie=U&&a.dev===U.dev&&a.ino===U.ino,Ae=U?.mtimeMs!==u7e;if(ie&&Ae&&h.autoRepair&&(N=0,U=null),!ie)if(p.overwrite)t.push(async()=>r.removePromise(s)),a=null;else return!1}let W=!U&&N===1?`${I}.${Math.floor(Math.random()*4294967296).toString(16).padStart(8,"0")}`:null,te=!1;return t.push(async()=>{if(!U&&(N===0&&await r.lockPromise(I,async()=>{let ie=await n.readFilePromise(c);await r.writeFilePromise(I,ie)}),N===1&&W)){let ie=await n.readFilePromise(c);await r.writeFilePromise(W,ie);try{await r.linkPromise(W,I)}catch(Ae){if(Ae.code==="EEXIST")te=!0,await r.unlinkPromise(W);else throw Ae}}a||await r.linkPromise(I,s)}),e.push(async()=>{U||(await r.lutimesPromise(I,gd,gd),S!==C&&await r.chmodPromise(I,S)),W&&!te&&await r.unlinkPromise(W)}),!1}async function p7e(t,e,r,s,a,n,c,f,p){if(a!==null)if(p.overwrite)t.push(async()=>r.removePromise(s)),a=null;else return!1;return t.push(async()=>{let h=await n.readFilePromise(c);await r.writeFilePromise(s,h)}),!0}async function h7e(t,e,r,s,a,n,c,f,p){return p.linkStrategy?.type==="HardlinkFromIndex"?A7e(t,e,r,s,a,n,c,f,p,p.linkStrategy):p7e(t,e,r,s,a,n,c,f,p)}async function g7e(t,e,r,s,a,n,c,f,p){if(a!==null)if(p.overwrite)t.push(async()=>r.removePromise(s)),a=null;else return!1;return t.push(async()=>{await r.symlinkPromise(ox(r.pathUtils,await n.readlinkPromise(c)),s)}),!0}var gd,u7e,P_=Ct(()=>{tl();gd=new Date(456789e3*1e3),u7e=gd.getTime()});function lx(t,e,r,s){let a=()=>{let n=r.shift();if(typeof n>"u")return null;let c=t.pathUtils.join(e,n);return Object.assign(t.statSync(c),{name:n,path:void 0})};return new L2(e,a,s)}var L2,B$=Ct(()=>{ix();L2=class{constructor(e,r,s={}){this.path=e;this.nextDirent=r;this.opts=s;this.closed=!1}throwIfClosed(){if(this.closed)throw I_()}async*[Symbol.asyncIterator](){try{let e;for(;(e=await this.read())!==null;)yield e}finally{await this.close()}}read(e){let r=this.readSync();return typeof e<"u"?e(null,r):Promise.resolve(r)}readSync(){return this.throwIfClosed(),this.nextDirent()}close(e){return this.closeSync(),typeof e<"u"?e(null):Promise.resolve()}closeSync(){this.throwIfClosed(),this.opts.onClose?.(),this.closed=!0}}});function v$(t,e){if(t!==e)throw new Error(`Invalid StatWatcher status: expected '${e}', got '${t}'`)}var S$,cx,D$=Ct(()=>{S$=Ie("events");S_();cx=class t extends S$.EventEmitter{constructor(r,s,{bigint:a=!1}={}){super();this.status="ready";this.changeListeners=new Map;this.startTimeout=null;this.fakeFs=r,this.path=s,this.bigint=a,this.lastStats=this.stat()}static create(r,s,a){let n=new t(r,s,a);return n.start(),n}start(){v$(this.status,"ready"),this.status="running",this.startTimeout=setTimeout(()=>{this.startTimeout=null,this.fakeFs.existsSync(this.path)||this.emit("change",this.lastStats,this.lastStats)},3)}stop(){v$(this.status,"running"),this.status="stopped",this.startTimeout!==null&&(clearTimeout(this.startTimeout),this.startTimeout=null),this.emit("stop")}stat(){try{return this.fakeFs.statSync(this.path,{bigint:this.bigint})}catch{let r=this.bigint?new rE:new tE;return sx(r)}}makeInterval(r){let s=setInterval(()=>{let a=this.stat(),n=this.lastStats;v_(a,n)||(this.lastStats=a,this.emit("change",a,n))},r.interval);return r.persistent?s:s.unref()}registerChangeListener(r,s){this.addListener("change",r),this.changeListeners.set(r,this.makeInterval(s))}unregisterChangeListener(r){this.removeListener("change",r);let s=this.changeListeners.get(r);typeof s<"u"&&clearInterval(s),this.changeListeners.delete(r)}unregisterAllChangeListeners(){for(let r of this.changeListeners.keys())this.unregisterChangeListener(r)}hasChangeListeners(){return this.changeListeners.size>0}ref(){for(let r of this.changeListeners.values())r.ref();return this}unref(){for(let r of this.changeListeners.values())r.unref();return this}}});function nE(t,e,r,s){let a,n,c,f;switch(typeof r){case"function":a=!1,n=!0,c=5007,f=r;break;default:({bigint:a=!1,persistent:n=!0,interval:c=5007}=r),f=s;break}let p=ux.get(t);typeof p>"u"&&ux.set(t,p=new Map);let h=p.get(e);return typeof h>"u"&&(h=cx.create(t,e,{bigint:a}),p.set(e,h)),h.registerChangeListener(f,{persistent:n,interval:c}),h}function dd(t,e,r){let s=ux.get(t);if(typeof s>"u")return;let a=s.get(e);typeof a>"u"||(typeof r>"u"?a.unregisterAllChangeListeners():a.unregisterChangeListener(r),a.hasChangeListeners()||(a.stop(),s.delete(e)))}function md(t){let e=ux.get(t);if(!(typeof e>"u"))for(let r of e.keys())dd(t,r)}var ux,x_=Ct(()=>{D$();ux=new WeakMap});function d7e(t){let e=t.match(/\r?\n/g);if(e===null)return P$.EOL;let r=e.filter(a=>a===`\r
53
`).length,s=e.length-r;return r>s?`\r
64
`:`

conductor.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"scripts": {
3+
"setup": "git submodule update --init && yarn",
4+
"run": "PORT=${CONDUCTOR_PORT:-3000} yarn dev"
5+
}
6+
}

eslint.config.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
33
const config = [
44
...nextCoreWebVitals,
55
{
6-
ignores: [".next/**", "out/**", "node_modules/**", "PrairieLearn/**"],
6+
ignores: [
7+
".next/**",
8+
".yarn/**",
9+
"out/**",
10+
"node_modules/**",
11+
"PrairieLearn/**",
12+
],
713
},
814
];
915

src/components/AshbyEmbed.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Head from "next/head";
2+
import Script from "next/script";
3+
4+
export const AshbyEmbed: React.FC = () => {
5+
return (
6+
<>
7+
<div id="ashby_embed"></div>
8+
<Script id="ashby-config">
9+
{`window.__ashbyBaseJobBoardUrl = "https://jobs.ashbyhq.com/prairielearn";`}
10+
</Script>
11+
<Script
12+
src="https://jobs.ashbyhq.com/prairielearn/embed?version=2"
13+
strategy="afterInteractive"
14+
/>
15+
</>
16+
);
17+
};

src/components/BlogMarkdownLayout.tsx

Lines changed: 125 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,67 @@
1-
import React from "react";
2-
import { MarkdownLayout } from "./MarkdownLayout";
3-
4-
export const BlogMarkdownLayout = MarkdownLayout;
5-
6-
import Image from "next/image";
1+
import React, { useState } from "react";
2+
import Image, { ImageProps } from "next/image";
73
import classNames from "classnames";
84
import ReactMarkdown from "react-markdown";
5+
import { MDXProvider } from "@mdx-js/react";
6+
import Head from "next/head";
7+
import Modal from "react-bootstrap/Modal";
8+
9+
import mdxComponents from "../lib/mdxComponents";
10+
import { PageBanner } from "./Banner";
11+
12+
interface BlogMarkdownLayoutProps {
13+
children: React.ReactNode;
14+
meta: {
15+
title: string;
16+
date?: string;
17+
author?: string;
18+
tags?: string[];
19+
ogImage?: ImageProps["src"];
20+
summary?: string;
21+
backText?: string;
22+
backHref?: string;
23+
};
24+
}
25+
26+
export const BlogMarkdownLayout: React.FC<BlogMarkdownLayoutProps> = ({
27+
children,
28+
meta,
29+
}) => {
30+
const { title, summary, ogImage, backText, backHref } = meta;
31+
if (!title) throw new Error("Missing title");
32+
33+
return (
34+
<React.Fragment>
35+
<Head>
36+
<title>{`${title} | PrairieLearn`}</title>
37+
<meta property="og:title" content={title} />
38+
{ogImage && (
39+
<meta
40+
property="og:image"
41+
content={
42+
typeof ogImage === "string"
43+
? ogImage
44+
: "default" in ogImage
45+
? ogImage.default.src
46+
: ogImage.src
47+
}
48+
/>
49+
)}
50+
</Head>
51+
52+
<PageBanner
53+
title={title}
54+
subtitle={summary}
55+
backText={backText}
56+
backHref={backHref}
57+
/>
58+
59+
<div className="container my-5">
60+
<MDXProvider components={mdxComponents}>{children}</MDXProvider>
61+
</div>
62+
</React.Fragment>
63+
);
64+
};
965

1066
export const BlogImage = ({
1167
src,
@@ -18,25 +74,70 @@ export const BlogImage = ({
1874
full?: boolean;
1975
caption?: string | React.ReactNode;
2076
}) => {
77+
const [showFullscreen, setShowFullscreen] = useState(false);
78+
2179
return (
22-
<figure className="w-100">
23-
<div className="d-flex justify-content-center">
24-
<Image
25-
src={src}
26-
alt={alt}
27-
className={classNames("img-fluid", !full && "w-50")}
28-
/>
29-
</div>
30-
{caption && (
31-
<figcaption className="text-muted small mt-2 text-center">
32-
{typeof caption === "string" ? (
33-
<ReactMarkdown>{caption}</ReactMarkdown>
34-
) : (
35-
caption
36-
)}
37-
</figcaption>
38-
)}
39-
</figure>
80+
<>
81+
<figure className="w-100">
82+
<div className="d-flex justify-content-center">
83+
<Image
84+
src={src}
85+
alt={alt}
86+
className={classNames(
87+
"img-fluid rounded shadow-sm",
88+
!full && "col-lg-6 col-md-8 col-12",
89+
)}
90+
onClick={() => setShowFullscreen(true)}
91+
style={{ cursor: "zoom-in" }}
92+
role="button"
93+
tabIndex={0}
94+
onKeyDown={(e) => {
95+
if (e.key === "Enter" || e.key === " ") {
96+
e.preventDefault();
97+
setShowFullscreen(true);
98+
}
99+
}}
100+
/>
101+
</div>
102+
{caption && (
103+
<figcaption className="text-muted small mt-2 text-center">
104+
{typeof caption === "string" ? (
105+
<ReactMarkdown>{caption}</ReactMarkdown>
106+
) : (
107+
caption
108+
)}
109+
</figcaption>
110+
)}
111+
</figure>
112+
113+
<Modal
114+
show={showFullscreen}
115+
onHide={() => setShowFullscreen(false)}
116+
fullscreen
117+
contentClassName="bg-transparent border-0"
118+
backdrop={false}
119+
>
120+
<Modal.Body
121+
className="p-0"
122+
onClick={() => setShowFullscreen(false)}
123+
style={{
124+
cursor: "zoom-out",
125+
backgroundColor: "rgba(0, 0, 0, 0.95)",
126+
position: "relative",
127+
width: "100vw",
128+
height: "100vh",
129+
}}
130+
>
131+
<Image
132+
src={src}
133+
alt={alt}
134+
fill
135+
sizes="100vw"
136+
style={{ objectFit: "contain", padding: "2.5vh 2.5vw" }}
137+
/>
138+
</Modal.Body>
139+
</Modal>
140+
</>
40141
);
41142
};
42143

src/components/Tag.module.scss

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,10 @@
2222
background-color: #0d6efd;
2323
color: white;
2424
border-color: #0d6efd;
25-
26-
&:hover {
27-
background-color: #0b5ed7;
28-
border-color: #0b5ed7;
29-
}
3025
}
3126

32-
.tag:not(.technical):hover {
33-
background-color: #dee2e6;
34-
border-color: #adb5bd;
27+
.release {
28+
background: #9d79d2;
29+
color: white;
30+
border: none;
3531
}

0 commit comments

Comments
 (0)