Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@astrojs/partytown": "^2.1.4",
"@astrojs/react": "^3.4.0",
"@directus/sdk": "^19.0.1",
"@formkit/auto-animate": "^0.9.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"astro": "^4.9.3",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions public/static_assets/downloads/archive/analytics/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const errorString = 'Undetermined';
export const maybe = (x) => {
switch (x) {
case null:
case undefined:
case '':
return errorString;
default:
return x;
}
};

export const text = async (url) => fetch(url).then((res) => res.text());

export const getIPFromCloudFlare = async () => {
const data = await text('https://www.cloudflare.com/cdn-cgi/trace');
let ipRegex = /ip=.+/;
let ip = data.match(ipRegex)[0].split('=')[1];
return ip;
};

export const getIPFromAmazon = async () => {
const data = await text('https://checkip.amazonaws.com/');
return data;
};

export const getIPFromIPify = async () => {
const data = await text('https://api.ipify.org/');
return data;
};

export const getIPAddress = async () => {
for (const func of [getIPFromAmazon, getIPFromIPify, getIPFromCloudFlare]) {
try {
const response = await func();
if (response) {
return response.trim();
}
} catch (error) {
return errorString;
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { maybe, getIPAddress } from './common.js';

const eventName = 'download_latest_banner_click';
const paramGetters = {
clicks: () => maybe(1),
value: () => maybe(1),
netlogo_version: () => {
return maybe(document.querySelector('meta[name="netlogo:version"]')?.getAttribute('content'));
},
timestamp: () => maybe(new Date().toISOString()),
document_title: () => maybe(document.title),
document_referrer: () => maybe(document.referrer),
navigator_user_agent: () => maybe(navigator.userAgent),
navigator_platform: () => maybe(navigator.platform),
navigator_language: () => maybe(navigator.language),
navigator_cookies_enabled: () => maybe(navigator.cookieEnabled ? 'true' : 'false'),
ip: () => getIPAddress(),
};

window.onDownloadLatestBannerClick = function () {
const params = {};
for (const [key, getter] of Object.entries(paramGetters)) {
params[key] = getter();
}
if (typeof gtag === 'function') {
gtag('event', eventName, params);
}
};

document.querySelector('#download-latest-banner')?.addEventListener('click', window.onDownloadLatestBannerClick);
70 changes: 70 additions & 0 deletions public/static_assets/downloads/archive/analytics/download.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { maybe, getIPAddress } from './common.js';

const formats = ['exe', 'msi', 'dmg', 'zip', 'tar.gz', 'tgz', 'jar'];
const formatsPattern = new RegExp(`\\.(${formats.join('|')})$`, 'i');

const eventName = 'download_netlogo';
const paramGetters = {
href: (_, href) => href,
format: (_, href) => {
const match = href.match(formatsPattern);
return maybe(match ? match[1].toLowerCase() : null);
},
version: () => {
return maybe(document.querySelector('meta[name="netlogo:version"]')?.getAttribute('content'));
},
download_platform: (anchor) => {
let platform = anchor.getAttribute('data-platform');
if (!platform) {
const row = anchor.closest('tr');
if (row) {
const cells = row.querySelectorAll('td');
for (const cell of cells) {
if (cell.querySelector('script') && !cell.querySelector('font')) {
continue;
} else if (cell.querySelector('font')) {
platform = maybe(cell.querySelector('font')?.textContent.trim().split('\n')[0]);
break;
}
const text = cell.textContent.trim();
if (text) {
platform = text;
break;
}
}
}
}
return maybe(platform);
},
timestamp: () => maybe(new Date().toISOString()),
document_title: () => maybe(document.title),
document_referrer: () => maybe(document.referrer),
navigator_user_agent: () => maybe(navigator.userAgent),
navigator_platform: () => maybe(navigator.platform),
navigator_language: () => maybe(navigator.language),
navigator_cookies_enabled: () => maybe(navigator.cookieEnabled ? 'true' : 'false'),
ip: () => getIPAddress(),
};

function handleDownloadLink(anchor, href) {
anchor.addEventListener('click', async function () {
const params = {};
for (const [key, getter] of Object.entries(paramGetters)) {
params[key] = await getter(anchor, href);
}
if (typeof gtag === 'function') {
gtag('event', eventName, params);
}
});
}

document.addEventListener('DOMContentLoaded', function () {
const anchors = document.querySelectorAll('a');
anchors.forEach((anchor) => {
const href = anchor.getAttribute('href');
const isDownloadLink = href && formatsPattern.test(href);
if (isDownloadLink) {
handleDownloadLink(anchor, href);
}
});
});
6 changes: 6 additions & 0 deletions public/static_assets/downloads/archive/analytics/gtag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
window.dataLayer = window.dataLayer || [];
function gtag(...args) {
dataLayer.push(args);
}
gtag('js', new Date());
gtag('config', 'G-5L2W88N305');
Binary file added public/static_assets/downloads/archive/donate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/static_assets/downloads/archive/os-aix.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/static_assets/downloads/archive/os-hp.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/static_assets/downloads/archive/os-mac.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/static_assets/downloads/archive/os-os2.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/static_assets/downloads/archive/os-unix.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/static_assets/downloads/archive/os-win.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/static_assets/downloads/archive/os-x.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/static_assets/downloads/archive/title.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions src/components/download/archive/archive-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function ArchivePage({ title, children }: ArchivePageProps) {
return (
<div className="container py-5 font-inter text-start">
<h1 className="fw-bold mb-4">{title}</h1>
{children}
</div>
)
}

interface ArchivePageProps {
title: string;
children?: React.ReactNode;
}

export default ArchivePage;
65 changes: 65 additions & 0 deletions src/components/download/archive/download-latest-banner.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.banner {
display: flex;
align-items: center;
justify-content: space-around;
gap: 1rem;

position: relative;

padding: 1rem;
margin: 1rem auto 2.5rem auto;

color: inherit;
background-color: #ffed29;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 8px;

text-decoration: none;

height: auto;
max-width: min(90%, 600px);

user-select: none;
transition: transform 0.2s;
}
.banner::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 10px;
height: 100%;
background-color: #e11818;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
transition: border-color 0.2s;
}
.banner:hover,
.banner:focus {
transform: translateY(-2px);
outline: none;
}
.logo {
height: 100%;
max-height: 70px;
max-width: 50%;
}
.decorate-as-link {
text-decoration: underline;
color: blue;
}
.text {
line-height: 1.5;
}

@media (max-width: 600px) {
.banner {
flex-direction: column;
text-align: center;
gap: 0.5rem;
}
.logo {
max-width: 80%;
height: auto;
}
}
19 changes: 19 additions & 0 deletions src/components/download/archive/download-latest-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import styles from './download-latest-banner.module.css';
import NetLogoOrgLogo from "../../../assets/NetLogoOrgLogo.svg";

export default function DownloadLatestBanner({ isLatest }: { isLatest: boolean }) {
if (isLatest) return null;
return (
<>
<a id="download-latest-banner" className={styles.banner} href="https://www.netlogo.org/download" target="_blank" rel="noopener noreferrer" aria-label="Older version of NetLogo. Click to download the latest version.">
<img src={NetLogoOrgLogo.src} alt="NetLogo Logo" className={styles.logo} />
<p className={styles.text}>
<strong>This is an older version of NetLogo</strong>
<br />
Download the latest version <span className={styles.decorateAsLink}>here</span>.
</p>
</a>
<script src="/static_assets/downloads/archive/analytics/download-latest-banner.js" type="module"></script>
</>
)
}
38 changes: 38 additions & 0 deletions src/components/download/archive/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useState } from 'react';
import type { VersionEntryProps, VersionFlag } from './version-entry';

export function useVersionFilter(
versions: VersionEntryProps[],
) {
// flagsFilter contains flags to HIDE (not show)
const [flagsFilter, setFlagsFilter] = useState<Set<VersionFlag>>(new Set(['BETA', "MILESTONE", "RC"]));

const toggleFlagFilter = (flag: VersionFlag) => {
console.log('Toggling flag filter for:', flag);
setFlagsFilter((prev) => {
const newSet = new Set(prev);
if (newSet.has(flag)) {
newSet.delete(flag);
} else {
newSet.add(flag);
}
return newSet;
});
};

const filteredVersions = versions.filter((version) => {
if (flagsFilter.size === 0) {
return true;
}
if (!version.flags || version.flags.length === 0) {
return true;
}
return !version.flags.some((flag) => flagsFilter.has(flag));
});

return {
flagsFilter,
toggleFlagFilter,
filteredVersions,
};
}
89 changes: 89 additions & 0 deletions src/components/download/archive/version-entry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// VersionEntry Component
interface VersionEntryProps {
// The version string, e.g., "6.2.0"
version: string;
// The release date string in a standard format, e.g., "2020-05-15"
releaseDate?: string;
// Optional URL to the version's download page
href?: string;
// Optional flags indicating special version types
flags?: VersionFlag[];
// Optional short description (0-2 sentences) or list of key features
blurb?: string | string[];
}

const VersionEntry = ({ version, releaseDate, flags = [], href, blurb }: VersionEntryProps) => {
const formattedDate = releaseDate ? formatReleaseDate(releaseDate) : undefined;

return (
<>
<a href={href} target="_blank">NetLogo {version}</a>
{formattedDate && (
<span className="timeline-date">{formattedDate}</span>
)}
{flags.length > 0 && (
<span className="mt-1">
{flags.map((flag) => (
<VersionFlagBadge key={flag} flag={flag} />
))}
</span>
)}
<Blurb text={blurb} />
</>
)
}

// Blurb Component
const Blurb = ({ text }: { text: string | string[] | undefined }) => {
if (!text) return null;

if (Array.isArray(text) && text.length > 0) {
return (
<ul className="timeline-blurb-list">
{text.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}

return <p className="timeline-description">{text}</p>;
};

// VersionFlagBadge Subcomponent
type VersionFlag = 'BETA' | 'RC' | 'REVISION' | 'LATEST' | "MILESTONE" | "PREVIEW";

const versionFlagStyles: { [key in VersionFlag]: string } = {
BETA: "badge bg-warning text-dark me-1 ms-2",
RC: "badge bg-info me-1 ms-2",
REVISION: "badge bg-secondary me-1 ms-2",
LATEST: "badge bg-success me-1 ms-2",
MILESTONE: "badge bg-primary me-1 ms-2",
PREVIEW: "badge bg-dark me-1 ms-2",
};

const versionFlagTexts: { [key in VersionFlag]: string } = {
BETA: "Beta",
RC: "Release Candidate",
REVISION: "Revision",
LATEST: "Latest",
MILESTONE: "Milestone",
PREVIEW: "Preview",
};


const VersionFlagBadge = ({ flag }: { flag: VersionFlag }) => {
let badgeText = versionFlagTexts[flag];
let badgeClass = versionFlagStyles[flag];

return <span className={badgeClass} title={badgeText} role="alert">{badgeText}</span>;
};

// Utilities
const formatReleaseDate = (dateString: string) => {
const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long' };
return new Date(dateString).toLocaleDateString(undefined, options);
};

export type { VersionEntryProps, VersionFlag };
export default VersionEntry;
Loading