Skip to content
Merged
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
57 changes: 57 additions & 0 deletions components/ContentFreezeBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";
import Logo from "components/Logo";
import { CheckCircleIcon } from "@heroicons/react/24/solid";

type Props = {
className?: string;
};

export default function ContentFreezeBanner({ className }: Props) {
return (
<div className={`border border-gray-200 rounded-lg overflow-hidden shadow-sm ${className || ""}`}>
<div className="bg-secondary px-6 py-5 flex items-center gap-4">
<Logo className="w-10 h-10 flex-shrink-0 [&_path]:fill-white" />
<div>
<h2 className="text-white text-lg font-semibold leading-snug">Content editing is now closed</h2>
<p className="text-blue-200 text-sm">BirdingHotspots.org | March 23, 2026</p>
</div>
</div>

<div className="p-6">
<p className="text-gray-700 mb-5">
The BirdingHotspots content freeze is now in effect. Editing has been disabled as we migrate all hotspot
content to eBird, where it will be available to the global birding community.
</p>

<div className="bg-gray-50 border border-gray-200 rounded-lg p-5 mb-5">
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">What happens next</h3>
<ul className="space-y-3">
<li className="flex items-start gap-3">
<CheckCircleIcon className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-700">Existing hotspot content is being migrated to eBird.</span>
</li>
<li className="flex items-start gap-3">
<CheckCircleIcon className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-700">
This site will remain accessible in read-only mode for at least 12 months.
</span>
</li>
<li className="flex items-start gap-3">
<CheckCircleIcon className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-700">
Editing will continue on eBird, with the same community-driven model you know.
</span>
</li>
</ul>
</div>

<hr className="mb-5" />

<p className="text-gray-600 text-sm">
Hotspot content will be editable on eBird starting in late April. Thank you for your contributions to
BirdingHotspots over the years.
</p>
</div>
</div>
);
}
9 changes: 7 additions & 2 deletions components/TinyMCE.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ type InputProps = {
name: string;
defaultValue?: string;
config?: any;
disabled?: boolean;
[x: string]: any;
};

const TinyMCE = ({ name, defaultValue, config, ...props }: InputProps) => {
const TinyMCE = ({ name, defaultValue, config, disabled, ...props }: InputProps) => {
const { control } = useFormContext();
return (
<div className="mt-1">
Expand All @@ -22,7 +23,11 @@ const TinyMCE = ({ name, defaultValue, config, ...props }: InputProps) => {
tinymceScriptSrc={process.env.NEXT_PUBLIC_DOMAIN + "/tinymce/tinymce.min.js"}
id={name}
initialValue={defaultValue || ""}
init={config || defaultConfig}
init={{
...(config || defaultConfig),
...(disabled && { toolbar: false }),
}}
disabled={disabled}
onEditorChange={onChange}
{...props}
/>
Expand Down
7 changes: 6 additions & 1 deletion hooks/useAvailableImgCount.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { useQuery } from "@tanstack/react-query";
import { ENABLE_PHOTO_SYNC } from "lib/config";

export default function useAvailableImgCount(locationId: string) {
const { data, isLoading, error, refetch } = useQuery<{ count: number }>({
queryKey: ["/api/count-ml-photos", { locationId }],
enabled: !!locationId,
enabled: !!locationId && ENABLE_PHOTO_SYNC,
});

if (!ENABLE_PHOTO_SYNC) {
return { count: "Unknown", isLoading: false, error: null, refetch };
}

const count = data?.count || 0;

return { count: count > 10 ? "> 10" : count.toString(), isLoading, error, refetch };
Expand Down
24 changes: 24 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
export const ENABLE_LEGACY_UPLOADS = false;
export const ENABLE_SUGGESTIONS = false;
export const ENABLE_EDITOR_WRITE = false;
export const ENABLE_ADMIN_WRITE = true;
export const ENABLE_SYNC = false;
export const ENABLE_PHOTO_SYNC = false;
export const ENABLE_ANALYTICS_LOGGING = false;

export const isWriteFrozen = (role?: string) => {
if (!role) return false;
if (role === "admin") return !ENABLE_ADMIN_WRITE;
return !ENABLE_EDITOR_WRITE;
};

export const assertWriteEnabled = (res: any, role?: string) => {
if (role === "admin" && !ENABLE_ADMIN_WRITE) {
res.status(403).json({ error: "Write operations are currently disabled" });
return false;
}
if (role !== "admin" && !ENABLE_EDITOR_WRITE) {
res.status(403).json({ error: "Write operations are currently disabled" });
return false;
}
return true;
};

export const PLAN_SECTION_HELP_TEXT = `Everything you need to know before you go. This section provides logistical information to plan an informed visit, including entrance fees, permit requirements, operating hours, directions, parking details, amenities, and accessibility notes. Include key details visitors should be aware of ahead of time—such as special rules and seasonal closures—that could impact their visit.`;

Expand Down
41 changes: 3 additions & 38 deletions lib/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,6 @@ export function capitalize(str: string) {
return words.join(" ");
}

export async function geocode(lat: number, lng: number) {
console.log("Geocoding", lat, lng);
try {
const request = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.NEXT_PUBLIC_GEOCODING_KEY}`
);
const response = await request.json();

let city = "";
let state = "";
let zip = "";
let road = "";
for (let i = 0; i < response.results[0].address_components.length; i++) {
for (let j = 0; j < response.results[0].address_components[i].types.length; j++) {
switch (response.results[0].address_components[i].types[j]) {
case "locality":
city = response.results[0].address_components[i].long_name;
break;
case "administrative_area_level_1":
state = response.results[0].address_components[i].long_name;
break;
case "postal_code":
zip = response.results[0].address_components[i].long_name;
break;
case "route":
road = response.results[0].address_components[i].long_name;
break;
}
}
}
return { city, state, zip };
} catch (error) {
console.error(error);
return { city: "", state: "", zip: "" };
}
}

export function scrollToAnchor(e: React.MouseEvent<HTMLAnchorElement>) {
e.preventDefault();
const anchor = e.currentTarget.getAttribute("href");
Expand Down Expand Up @@ -272,7 +235,9 @@ export const getStaticMap = (markers: { species?: number; lat: number; lng: numb
};

export async function getEbirdHotspot(locationId: string) {
const response = await fetch(`https://api.ebird.org/v2/ref/hotspot/info/${locationId}?key=${process.env.EBIRD_API_KEY}`);
const response = await fetch(
`https://api.ebird.org/v2/ref/hotspot/info/${locationId}?key=${process.env.EBIRD_API_KEY}`
);
if (response.status === 200) {
return await response.json();
}
Expand Down
47 changes: 27 additions & 20 deletions lib/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,16 +770,20 @@ export async function deleteHotspot(hotspot: HotspotType) {
export const getHotspotImages = async (locationId: string) => {
await connect();

const { ENABLE_PHOTO_SYNC } = await import("lib/config");

const hotspotQuery = Hotspot.findOne({ locationId }, [
"featuredImg",
"images",
"featuredImg1",
"featuredImg2",
"featuredImg3",
"featuredImg4",
]).lean();

const [ebirdImages, hotspot] = await Promise.all([
getBestImages(locationId as string),
Hotspot.findOne({ locationId }, [
"featuredImg",
"images",
"featuredImg1",
"featuredImg2",
"featuredImg3",
"featuredImg4",
]).lean(),
ENABLE_PHOTO_SYNC ? getBestImages(locationId as string) : Promise.resolve([]),
hotspotQuery,
]);

if (!hotspot) throw new Error("Hotspot not found");
Expand All @@ -792,9 +796,10 @@ export const getHotspotImages = async (locationId: string) => {
(it): it is MlImage => !!it
);

const latestFeaturedImgData = currentFeaturedMlImages.length
? (await getImages(currentFeaturedMlImages.map((it) => it.id))) || []
: [];
const latestFeaturedImgData =
ENABLE_PHOTO_SYNC && currentFeaturedMlImages.length
? (await getImages(currentFeaturedMlImages.map((it) => it.id))) || []
: [];

const featuredMlImages = currentFeaturedMlImages.map((it) => {
const latestData = latestFeaturedImgData.find((latest) => latest.id === it.id);
Expand Down Expand Up @@ -823,14 +828,16 @@ export const getHotspotImages = async (locationId: string) => {
const shouldAddFeaturedImg = !hotspot.featuredImg && newFeaturedImg;
const shouldRemoveFeaturedImg = !newFeaturedImg && hotspot.featuredImg;

if (shouldUpdateFeaturedImg || shouldAddFeaturedImg) {
await Hotspot.updateOne({ locationId }, { featuredImg: newFeaturedImg });
} else if (shouldRemoveFeaturedImg) {
const legacyFeaturedImg = legacyImages[0];
await Hotspot.updateOne(
{ locationId },
legacyFeaturedImg ? { featuredImg: legacyFeaturedImg } : { $unset: { featuredImg: "" } }
);
if (ENABLE_PHOTO_SYNC) {
if (shouldUpdateFeaturedImg || shouldAddFeaturedImg) {
await Hotspot.updateOne({ locationId }, { featuredImg: newFeaturedImg });
} else if (shouldRemoveFeaturedImg) {
const legacyFeaturedImg = legacyImages[0];
await Hotspot.updateOne(
{ locationId },
legacyFeaturedImg ? { featuredImg: legacyFeaturedImg } : { $unset: { featuredImg: "" } }
);
}
}
return combinedImages;
};
9 changes: 1 addition & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
"@types/node": "^17.0.36",
"@types/nodemailer": "^6.4.4",
"@types/react": "^18.0.9",
"@types/react-geocode": "^0.2.0",
"@types/react-highlight-words": "^0.16.4",
"@types/react-image-gallery": "^1.0.5",
"@types/uuid": "^8.3.4",
Expand Down
6 changes: 6 additions & 0 deletions pages/api/admin/user/join.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { NextApiRequest, NextApiResponse } from "next";
import admin from "lib/firebaseAdmin";
import Profile from "models/Profile";
import { ENABLE_EDITOR_WRITE } from "lib/config";

export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
if (!ENABLE_EDITOR_WRITE) {
res.status(503).json({ error: "Editor registration is currently disabled" });
return;
}

try {
const { password, inviteCode } = req.body;
const profile = await Profile.findOne({ inviteCode });
Expand Down
2 changes: 2 additions & 0 deletions pages/api/article/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import connect from "lib/mongo";
import Article from "models/Article";
import secureApi from "lib/secureApi";
import { canEdit } from "lib/helpers";
import { assertWriteEnabled } from "lib/config";

export default secureApi(async (req, res, token) => {
if (!assertWriteEnabled(res, token.role)) return;
const { id }: any = req.query;

await connect();
Expand Down
2 changes: 2 additions & 0 deletions pages/api/article/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import connect from "lib/mongo";
import Article from "models/Article";
import secureApi from "lib/secureApi";
import { canEdit } from "lib/helpers";
import { assertWriteEnabled } from "lib/config";

export default secureApi(async (req, res, token) => {
if (!assertWriteEnabled(res, token.role)) return;
const { isNew }: any = req.query;
const { data, id } = req.body;

Expand Down
5 changes: 5 additions & 0 deletions pages/api/check-ml-ids.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getImages } from "lib/ml";
import { ENABLE_PHOTO_SYNC } from "lib/config";

export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
if (!ENABLE_PHOTO_SYNC) {
return res.status(200).json({ success: true, missingIds: [] });
}

const assetIdsStr = req.query.assetIds as string | undefined;
const assetIds = assetIdsStr?.split(",") || [];
const cleanAssetIds = assetIds.map((id) => Number(id));
Expand Down
5 changes: 5 additions & 0 deletions pages/api/count-ml-photos.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getImageCount } from "lib/ml";
import { ENABLE_PHOTO_SYNC } from "lib/config";

export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
if (!ENABLE_PHOTO_SYNC) {
return res.status(200).json({ success: true, count: 0 });
}

const { locationId }: any = req.query;

if (!locationId) {
Expand Down
2 changes: 2 additions & 0 deletions pages/api/drive/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import Hotspot from "models/Hotspot";
import Logs from "models/Log";
import secureApi from "lib/secureApi";
import { canEdit } from "lib/helpers";
import { assertWriteEnabled } from "lib/config";

export default secureApi(async (req, res, token) => {
if (!assertWriteEnabled(res, token.role)) return;
const { id }: any = req.query;

await connect();
Expand Down
2 changes: 2 additions & 0 deletions pages/api/drive/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import Hotspot from "models/Hotspot";
import Logs from "models/Log";
import secureApi from "lib/secureApi";
import { canEdit } from "lib/helpers";
import { assertWriteEnabled } from "lib/config";

export default secureApi(async (req, res, token) => {
if (!assertWriteEnabled(res, token.role)) return;
const { isNew }: any = req.query;
const { data, id } = req.body;

Expand Down
2 changes: 2 additions & 0 deletions pages/api/file/add-streetview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import Hotspot from "models/Hotspot";
import connect from "lib/mongo";
import secureApi from "lib/secureApi";
import { region, endpoint, bucket } from "lib/s3";
import { assertWriteEnabled } from "lib/config";

export default secureApi(async (req, res, token) => {
if (!assertWriteEnabled(res, token.role)) return;
const s3 = new S3Client({
credentials: {
accessKeyId: process.env.S3_KEY || "",
Expand Down
2 changes: 2 additions & 0 deletions pages/api/group/add-hotspot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import Logs from "models/Log";
import secureApi from "lib/secureApi";
import { canEdit } from "lib/helpers";
import dayjs from "dayjs";
import { assertWriteEnabled } from "lib/config";

export default secureApi(async (req, res, token) => {
if (!assertWriteEnabled(res, token.role)) return;
const { groupLocationId, hotspotId } = req.body;

if (!groupLocationId || !hotspotId) {
Expand Down
Loading