diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..4fdd47d
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,9 @@
+# Cosmic CMS – get these from your Cosmic bucket dashboard
+# https://app.cosmicjs.com → your bucket → Settings → API Keys
+COSMIC_BUCKET_SLUG=your-bucket-slug
+COSMIC_READ_KEY=your-read-key
+COSMIC_WRITE_KEY=your-write-key
+
+# Behold.so Instagram feed – public URL, no secret. Drives the homepage News & Announcements section.
+# Find the feed URL in your Behold dashboard: https://app.behold.so/
+NEXT_PUBLIC_BEHOLD_FEED_URL=https://feeds.behold.so/your-feed-id
diff --git a/.github/workflows/preview.yaml b/.github/workflows/preview.yaml
new file mode 100644
index 0000000..ec528a8
--- /dev/null
+++ b/.github/workflows/preview.yaml
@@ -0,0 +1,25 @@
+name: Vercel Preview Deployment
+env:
+ VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
+ VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
+ COSMIC_BUCKET_SLUG: ${{ secrets.COSMIC_BUCKET_SLUG }}
+ COSMIC_READ_KEY: ${{ secrets.COSMIC_READ_KEY }}
+ COSMIC_WRITE_KEY: ${{ secrets.COSMIC_WRITE_KEY }}
+ NEXT_PUBLIC_BEHOLD_FEED_URL: ${{ secrets.NEXT_PUBLIC_BEHOLD_FEED_URL }}
+on:
+ push:
+ branches-ignore:
+ - main
+jobs:
+ Deploy-Preview:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install Vercel CLI
+ run: npm install --global vercel@latest
+ - name: Pull Vercel Environment Information
+ run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Build Project Artifacts
+ run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Deploy Project Artifacts to Vercel
+ run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml
new file mode 100644
index 0000000..b207f8c
--- /dev/null
+++ b/.github/workflows/production.yaml
@@ -0,0 +1,25 @@
+name: Vercel Production Deployment
+env:
+ VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
+ VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
+ COSMIC_BUCKET_SLUG: ${{ secrets.COSMIC_BUCKET_SLUG }}
+ COSMIC_READ_KEY: ${{ secrets.COSMIC_READ_KEY }}
+ COSMIC_WRITE_KEY: ${{ secrets.COSMIC_WRITE_KEY }}
+ NEXT_PUBLIC_BEHOLD_FEED_URL: ${{ secrets.NEXT_PUBLIC_BEHOLD_FEED_URL }}
+on:
+ push:
+ branches:
+ - main
+jobs:
+ Deploy-Production:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install Vercel CLI
+ run: npm install --global vercel@latest
+ - name: Pull Vercel Environment Information
+ run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Build Project Artifacts
+ run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Deploy Project Artifacts to Vercel
+ run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
diff --git a/.gitignore b/.gitignore
index e25f3d5..d084156 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
+!.env.example
# vercel
.vercel
diff --git a/app/components/InstagramPostCard.tsx b/app/components/InstagramPostCard.tsx
deleted file mode 100644
index ed49849..0000000
--- a/app/components/InstagramPostCard.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-type Props = {
- imageUrl?: string;
- username?: string;
- caption?: string;
- likes?: number;
- postUrl?: string;
-};
-
-export default function InstagramPostCard({
- imageUrl,
- username = "mindmoralitylab",
- caption = "",
- likes = 0,
- postUrl = "#",
-}: Props) {
- return (
-
- {/* Header */}
-
-
- {/* Image */}
-
- {imageUrl ? (
-
- ) : (
-
- )}
-
-
- {/* Actions */}
-
-
- {/* Likes */}
- {likes > 0 && (
-
- {likes.toLocaleString()} likes
-
- )}
-
- {/* Caption */}
- {caption && (
-
- {username}
- {caption}
-
- )}
-
- {/* View on Instagram */}
-
- View on Instagram
-
-
- );
-}
diff --git a/app/page.tsx b/app/page.tsx
index 1e442b5..6a73908 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -17,17 +17,29 @@ const page_to_link = new Map([
["students", "/get-involved/students"],
]);
-export default function Home() {
- const slides = ["/slideshow1.png", "/slideshow1.png", "/slideshow1.png"];
- // Clone of first slide appended so we can slide into it, then snap back invisibly
- const extendedSlides = [...slides, slides[0]];
+type BeholdPost = {
+ id: string;
+ timestamp: string;
+ permalink: string;
+ mediaType: "IMAGE" | "VIDEO" | "CAROUSEL_ALBUM";
+ mediaUrl: string;
+ thumbnailUrl?: string | null;
+ caption?: string;
+};
+
+const formatPostDate = (iso: string) =>
+ new Date(iso).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ });
+export default function Home() {
const [home, setHome] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
- const [current, setCurrent] = useState(0);
- const [animated, setAnimated] = useState(true);
+ const [instagramPosts, setInstagramPosts] = useState(null);
useEffect(() => {
const fetchData = async () => {
@@ -48,33 +60,14 @@ export default function Home() {
}, []);
useEffect(() => {
- const interval = setInterval(() => {
- setCurrent((prev) => prev + 1);
- }, 4000);
- return () => clearInterval(interval);
+ const feedUrl = process.env.NEXT_PUBLIC_BEHOLD_FEED_URL;
+ if (!feedUrl) return;
+ fetch(feedUrl)
+ .then((r) => (r.ok ? r.json() : null))
+ .then((data) => setInstagramPosts(data?.posts ?? null))
+ .catch(() => setInstagramPosts(null));
}, []);
- useEffect(() => {
- if (current === extendedSlides.length - 1) {
- // After sliding into the clone, snap back to real first slide without animation
- const timer = setTimeout(() => {
- setAnimated(false);
- setCurrent(0);
- }, 700);
- return () => clearTimeout(timer);
- }
- }, [current]);
-
- useEffect(() => {
- if (!animated) {
- // Re-enable animation after the snap
- const timer = setTimeout(() => setAnimated(true), 50);
- return () => clearTimeout(timer);
- }
- }, [animated]);
-
- const activeIndex = current === extendedSlides.length - 1 ? 0 : current;
-
if (loading) {
return (
@@ -102,42 +95,21 @@ export default function Home() {
`}
-
-
- {extendedSlides.map((slide, i) => (
-
-
-
- ))}
-
-
-
- {slides.map((_, i) => (
- setCurrent(i)}
- className={`w-2 h-2 rounded-full transition-colors duration-300 ${i === activeIndex ? "bg-[#F2AD3D]" : "bg-[#459A9F]"}`}
- />
- ))}
-
-
+
-
+
- The Mind & Morality Lab is a developmental psychology lab at Brown
- University in the Department of Cognitive & Psychological Sciences.
+ {home.metadata.lab_header}
- We focus on understanding the psychological roots of human morality
- through an interdisciplinary lens, drawing on philosophical, legal,
- and psychological perspectives in our work with both children and
- adults.
+ {home.metadata.lab_description}
@@ -148,18 +120,27 @@ export default function Home() {
News & Announcements
-
-
-
+ {instagramPosts && instagramPosts.length >= 3 ? (
+ instagramPosts.slice(0, 3).map((post) => (
+
+ ))
+ ) : (
+ <>
+
+
+
+ >
+ )}
diff --git a/components/InstagramPostCard.tsx b/components/InstagramPostCard.tsx
index 67e4486..717603c 100644
--- a/components/InstagramPostCard.tsx
+++ b/components/InstagramPostCard.tsx
@@ -2,7 +2,7 @@ type Props = {
imageUrl?: string;
username?: string;
caption?: string;
- likes?: number;
+ date?: string;
postUrl?: string;
};
@@ -10,32 +10,11 @@ export default function InstagramPostCard({
imageUrl,
username = "mindmoralitylab",
caption = "",
- likes = 0,
+ date,
postUrl = "#",
}: Props) {
return (
-
- {/* Header */}
-
-
{/* Image */}
{imageUrl ? (
@@ -55,45 +34,36 @@ export default function InstagramPostCard({
)}
- {/* Actions */}
-
-
-
-
-
-
-
-
-
-
-
+ {/* User + date row */}
+
+
+ M
+
+
+ {username}
+ {date && {date} }
+
+
+
+
+
+
+
+
- {/* Likes */}
- {likes > 0 && (
-
- {likes.toLocaleString()} likes
-
- )}
-
{/* Caption */}
{caption && (
-
- {username}
+
{caption}
)}
-
- {/* View on Instagram */}
-
- View on Instagram
-
-
);
}
diff --git a/components/Slideshow.tsx b/components/Slideshow.tsx
index 1e57fdf..b3ca862 100644
--- a/components/Slideshow.tsx
+++ b/components/Slideshow.tsx
@@ -58,7 +58,7 @@ export default function Slideshow({ images }: SlideshowProps) {
setCurrent(i)}
- className={`w-2 h-2 rounded-full ${i === current ? "bg-teal-500" : "bg-gray-300"}`}
+ className={`w-2 h-2 rounded-full transition-colors duration-300 ${i === current ? "bg-[#F2AD3D]" : "bg-[#459A9F]"}`}
aria-label={`Go to slide ${i + 1}`}
/>
))}
diff --git a/components/StaffCard.tsx b/components/StaffCard.tsx
index 8427981..db7bc2b 100644
--- a/components/StaffCard.tsx
+++ b/components/StaffCard.tsx
@@ -1,4 +1,4 @@
-import { TeamMember } from "@/services/CosmicServices";
+import { TeamMember } from "@/services/CosmicTypes";
type Props = {
member: TeamMember;
diff --git a/components/UndergradCard.tsx b/components/UndergradCard.tsx
index a15e1c3..8f75174 100644
--- a/components/UndergradCard.tsx
+++ b/components/UndergradCard.tsx
@@ -1,4 +1,4 @@
-import { TeamMember } from "@/services/CosmicServices";
+import { TeamMember } from "@/services/CosmicTypes";
type Props = {
member: TeamMember;
diff --git a/next.config.ts b/next.config.ts
index 609830b..5a1424d 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,9 +1,11 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
images: {
- domains: ['imgix.cosmicjs.com'],
+ remotePatterns: [
+ { protocol: "https", hostname: "imgix.cosmicjs.com" },
+ { protocol: "https", hostname: "cdn.cosmicjs.com" },
+ ],
},
};
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..45c873b
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,5 @@
+{
+ "git": {
+ "deploymentEnabled": false
+ }
+}