diff --git a/.gitignore b/.gitignore
index 34f9dfe..514c59a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,7 @@ yarn-error.*
# typescript
*.tsbuildinfo
+tsconfig.json
app-example
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 230c34e..cb26eff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## [1.2.7] - 2026-06-02 — Fix: Pattern detail expand animation and iOS native build support
+
+Fixes a native-only bug where the pattern detail card failed to expand in Expo Go and on-device builds. Adds iOS native build configuration and documents the native and release build workflows.
+
+### Mobile (`apps/mobile`)
+
+- Fixed `TopPatternsSection` expand animation on native: moved content measurement to an off-screen, opacity-0 clone outside the `height: 0` animated container (Yoga constrains child layout to the parent's explicit height, so `onLayout` always reported `h = 0`). Height is cached per pattern and animations are driven directly via `useSharedValue` to skip the `setState → re-render → useEffect` chain that caused visible jank on Android.
+- Added `bundleIdentifier: "com.rlyhan.touchgrass"` to `app.json` `ios` config, required for `expo run:ios` native builds.
+- Updated `ios` and `android` scripts to `expo run:ios` / `expo run:android` for native dev builds.
+
+### Storybook (`apps/mobile`)
+
+- Added stories for the four previously uncovered components: `PatternRing` (default, selected, dimmed, high/low match, 3-ring row), `PatternMatchAccordion` (default, alt pattern, long description), `TopPatternsSection` (default, close-matches, low-confidence), and `PortableText` (rich marks, single paragraph, custom className, empty).
+
+### Docs
+
+- Added physical-device Expo Go setup (two-terminal workflow), native dev build notes, and a release build command (`EXPO_PUBLIC_API_BASE_URL=http://localhost:3000 npx expo run:ios --configuration Release`) for accurate animation and performance testing to `README.md`.
+
## [1.2.6] - 2026-05-27 — Chore: Broaden recommendation surface and pattern affinity
Expands the recommendations list from 3 to 10 to give users more activities to browse while adjacent discovery features are still in flight, and widens each mock activity's `related_types` so secondary pattern affinity better reflects the personality dimensions a user actually shares with an activity.
diff --git a/README.md b/README.md
index 103cde5..6d82db8 100644
--- a/README.md
+++ b/README.md
@@ -66,14 +66,41 @@ npm start
This runs `@touchgrass/core` (Express on port 3000) and `@touchgrass/mobile` (Expo) concurrently. Both must be running for auth, profiles, and recommendations to work.
-**Platform-specific:**
+> **Note:** `npm start` uses `concurrently`, which prefixes log output and breaks QR code rendering in the terminal. If you need to scan a QR code to open the app in Expo Go on a physical device, run the two servers in separate terminals instead (see below).
+
+**Physical device (Expo Go) — separate terminals:**
+
+```bash
+# Terminal 1 — API server
+npm run dev --workspace=@touchgrass/core
+
+# Terminal 2 — Expo dev server (QR code renders correctly here)
+cd apps/mobile && npx expo start
+```
+
+Then scan the QR code with the **Expo Go app** on your device (not the native camera).
+
+**Platform-specific (simulators/emulators):**
```bash
-npm run ios # iOS Simulator
-npm run android # Android Emulator
+npm run ios # iOS Simulator — native dev build (requires bundleIdentifier in app.json)
+npm run android # Android Emulator — native dev build
npm run web # Browser at http://localhost:8081
```
+> **Note:** `npm run ios` / `npm run android` compile a native development build. This is slower to start than Expo Go but reflects real device behaviour more accurately. The `bundleIdentifier` (`com.rlyhan.touchgrass`) in `apps/mobile/app.json` is required for these commands.
+
+**Release build (accurate performance testing — animations, transitions):**
+
+Expo Go and native dev builds both run JavaScript in debug mode, which can make animations appear slow. For a production-accurate test, build in release mode:
+
+```bash
+cd apps/mobile
+EXPO_PUBLIC_API_BASE_URL=http://localhost:3000 npx expo run:ios --configuration Release
+```
+
+This must be run from `apps/mobile`, not the repo root.
+
**Run workspaces individually:**
```bash
diff --git a/apps/mobile/.storybook/index.ts b/apps/mobile/.storybook/index.ts
index 9eb4fba..7940d65 100644
--- a/apps/mobile/.storybook/index.ts
+++ b/apps/mobile/.storybook/index.ts
@@ -1,11 +1,19 @@
-import AsyncStorage from "@react-native-async-storage/async-storage"
-
import { view } from "./storybook.requires"
const StorybookUIRoot = view.getStorybookUI({
storage: {
- getItem: AsyncStorage.getItem,
- setItem: AsyncStorage.setItem,
+ getItem: async (key: string) => {
+ try {
+ return (globalThis as any).localStorage?.getItem(key) ?? null
+ } catch {
+ return null
+ }
+ },
+ setItem: async (key: string, value: string) => {
+ try {
+ ;(globalThis as any).localStorage?.setItem(key, value)
+ } catch {}
+ },
},
})
diff --git a/apps/mobile/.storybook/preview.tsx b/apps/mobile/.storybook/preview.tsx
index b1b2372..940d1ec 100644
--- a/apps/mobile/.storybook/preview.tsx
+++ b/apps/mobile/.storybook/preview.tsx
@@ -3,6 +3,7 @@ import { SafeAreaProvider } from "react-native-safe-area-context"
import type { Preview } from "@storybook/react-native"
import "../global.css"
+import "./storybook-web.css"
const preview: Preview = {
decorators: [
diff --git a/apps/mobile/.storybook/storybook-web.css b/apps/mobile/.storybook/storybook-web.css
new file mode 100644
index 0000000..96c89ad
--- /dev/null
+++ b/apps/mobile/.storybook/storybook-web.css
@@ -0,0 +1,12 @@
+/* Override the mobile-phone frame applied in global.css */
+@media screen and (min-width: 768px) {
+ #root {
+ max-width: 100% !important;
+ box-shadow: none !important;
+ }
+
+ body {
+ background-color: #ffffff !important;
+ align-items: stretch !important;
+ }
+}
diff --git a/apps/mobile/__tests__/top-patterns-section.test.tsx b/apps/mobile/__tests__/top-patterns-section.test.tsx
index 90cc661..8c22fb7 100644
--- a/apps/mobile/__tests__/top-patterns-section.test.tsx
+++ b/apps/mobile/__tests__/top-patterns-section.test.tsx
@@ -1,4 +1,10 @@
-import { act, fireEvent, render, screen } from "@testing-library/react-native"
+import {
+ act,
+ fireEvent,
+ render,
+ screen,
+ within,
+} from "@testing-library/react-native"
// Replace the animated ring with a simple text stub so we don't have to render
// the SVG or run the reanimated animation in tests.
@@ -57,7 +63,11 @@ describe("TopPatternsSection", () => {
fireEvent.press(
screen.getByRole("button", { name: `Show details for ${pattern.name}` }),
)
- expect(screen.getByText(pattern.shortDescription)).toBeTruthy()
+ expect(
+ within(screen.getByTestId("pattern-detail-panel")).getByText(
+ pattern.shortDescription,
+ ),
+ ).toBeTruthy()
})
it("hides the description when the selected ring is tapped again", () => {
@@ -93,12 +103,13 @@ describe("TopPatternsSection", () => {
fireEvent.press(
screen.getByRole("button", { name: `Show details for ${first.name}` }),
)
- expect(screen.getByText(first.shortDescription)).toBeTruthy()
+ const panel = () => screen.getByTestId("pattern-detail-panel")
+ expect(within(panel()).getByText(first.shortDescription)).toBeTruthy()
fireEvent.press(
screen.getByRole("button", { name: `Show details for ${second.name}` }),
)
- expect(screen.queryByText(first.shortDescription)).toBeNull()
- expect(screen.getByText(second.shortDescription)).toBeTruthy()
+ expect(within(panel()).queryByText(first.shortDescription)).toBeNull()
+ expect(within(panel()).getByText(second.shortDescription)).toBeTruthy()
})
})
diff --git a/apps/mobile/app.json b/apps/mobile/app.json
index e3d8397..23e4f4d 100644
--- a/apps/mobile/app.json
+++ b/apps/mobile/app.json
@@ -12,7 +12,8 @@
"policy": "sdkVersion"
},
"ios": {
- "supportsTablet": true
+ "supportsTablet": true,
+ "bundleIdentifier": "com.rlyhan.touchgrass"
},
"android": {
"adaptiveIcon": {
diff --git a/apps/mobile/app/(authed)/_layout.tsx b/apps/mobile/app/(authed)/_layout.tsx
index c5a2e3c..0690487 100644
--- a/apps/mobile/app/(authed)/_layout.tsx
+++ b/apps/mobile/app/(authed)/_layout.tsx
@@ -16,7 +16,7 @@ export default function AuthedLayout() {
}
if (!session?.user) {
- return
+ return
}
return
diff --git a/apps/mobile/app/(authed)/activities/[slug].tsx b/apps/mobile/app/(authed)/activities/[slug].tsx
index ac64c7b..763c258 100644
--- a/apps/mobile/app/(authed)/activities/[slug].tsx
+++ b/apps/mobile/app/(authed)/activities/[slug].tsx
@@ -16,6 +16,7 @@ import { PortableText } from "@/components/ui/portable-text"
// import { PrimaryButton } from "@/components/ui/primary-button"
import { resolveDominantPattern } from "@/lib/patterns/cache"
import { usePatternWeights } from "@/lib/patterns/use-pattern-weights"
+import { UnauthenticatedError } from "@/lib/auth/errors"
import {
fetchActivityBySlug,
getCachedActivity,
@@ -76,8 +77,12 @@ export default function ActivityDetailPage() {
setActivityStatus((prev) => (prev === "ready" ? prev : "not-found"))
}
})
- .catch(() => {
+ .catch((err) => {
if (cancelled) return
+ if (err instanceof UnauthenticatedError) {
+ router.replace("/sign-in" as Href)
+ return
+ }
// Network error: keep showing cached copy if we had one.
setActivityStatus((prev) => (prev === "ready" ? prev : "error"))
})
diff --git a/apps/mobile/app/(authed)/recommendations/index.tsx b/apps/mobile/app/(authed)/recommendations/index.tsx
index 85e86db..8618bc1 100644
--- a/apps/mobile/app/(authed)/recommendations/index.tsx
+++ b/apps/mobile/app/(authed)/recommendations/index.tsx
@@ -1,5 +1,5 @@
import { router, type Href } from "expo-router"
-import { useCallback, useState } from "react"
+import { useCallback, useMemo, useState } from "react"
import { ActivityIndicator, FlatList, Pressable, Text, View } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
@@ -15,6 +15,7 @@ import {
import {
fetchRecommendations,
ProfileNotFoundError,
+ UnauthenticatedError,
} from "@/lib/recommendations/api"
import { colors } from "@/lib/theme/colors"
import { useAsyncData } from "@/lib/use-async-data"
@@ -29,6 +30,10 @@ export default function RecommendationsPage() {
try {
return await fetchRecommendations()
} catch (err) {
+ if (err instanceof UnauthenticatedError) {
+ router.replace("/sign-in" as Href)
+ return []
+ }
// The user is authenticated but has no profile — most likely because
// onboarding was interrupted before profile creation completed. Send
// them back to finish it rather than leaving them on a dead-end error.
@@ -41,6 +46,26 @@ export default function RecommendationsPage() {
}, [])
const { data: recommendations = [], status, reload } = useAsyncData(fetcher)
+ const patternWeights = getCachedPatternWeights()
+
+ const listHeader = useMemo(
+ () => (
+ <>
+
+
+
+ {patternWeights ? (
+
+
+
+ ) : null}
+
+ Your recommendations
+
+ >
+ ),
+ [patternWeights],
+ )
const handleSignOut = useCallback(async () => {
setSigningOut(true)
@@ -117,24 +142,7 @@ export default function RecommendationsPage() {
ItemSeparatorComponent={ItemSeparator}
initialNumToRender={5}
contentContainerStyle={{ paddingHorizontal: 20, paddingTop: 24, paddingBottom: 32 }}
- ListHeaderComponent={
- <>
-
-
-
- {(() => {
- const weights = getCachedPatternWeights()
- return weights ? (
-
-
-
- ) : null
- })()}
-
- Your recommendations
-
- >
- }
+ ListHeaderComponent={listHeader}
ListFooterComponent={
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {}
+
+export const Visionary: Story = {
+ args: {
+ patternName: "Enchanting Visionary",
+ shortDescription:
+ "Imaginative, expressive, and drawn to novel ideas and aesthetics.",
+ },
+}
+
+export const LongDescription: Story = {
+ args: {
+ patternName: "Enlightened Traditionalist",
+ shortDescription:
+ "Grounded, principled, and reflective. You value time-tested approaches " +
+ "while staying open to insight, blending steadiness with a thoughtful " +
+ "curiosity about how things came to be the way they are.",
+ },
+}
diff --git a/apps/mobile/components/patterns/pattern-ring.stories.tsx b/apps/mobile/components/patterns/pattern-ring.stories.tsx
new file mode 100644
index 0000000..f0602b8
--- /dev/null
+++ b/apps/mobile/components/patterns/pattern-ring.stories.tsx
@@ -0,0 +1,59 @@
+import { View } from "react-native"
+import type { Meta, StoryObj } from "@storybook/react-native"
+
+import { PatternRing } from "./pattern-ring"
+
+const meta = {
+ title: "Patterns/PatternRing",
+ component: PatternRing,
+ args: {
+ percent: 78,
+ label: "Personable",
+ isSelected: false,
+ anySelected: false,
+ },
+ argTypes: {
+ percent: { control: { type: "range", min: 0, max: 100, step: 1 } },
+ label: { control: "text" },
+ duration: { control: { type: "range", min: 0, max: 2000, step: 50 } },
+ isSelected: { control: "boolean" },
+ anySelected: { control: "boolean" },
+ },
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {}
+
+export const Selected: Story = {
+ args: { isSelected: true, anySelected: true },
+}
+
+export const Dimmed: Story = {
+ args: { isSelected: false, anySelected: true },
+}
+
+export const HighMatch: Story = {
+ args: { percent: 95, label: "Enchanting Visionary" },
+}
+
+export const LowMatch: Story = {
+ args: { percent: 12, label: "Independent-Distant" },
+}
+
+export const Row: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+}
diff --git a/apps/mobile/components/patterns/top-patterns-section.stories.tsx b/apps/mobile/components/patterns/top-patterns-section.stories.tsx
new file mode 100644
index 0000000..ecfff92
--- /dev/null
+++ b/apps/mobile/components/patterns/top-patterns-section.stories.tsx
@@ -0,0 +1,52 @@
+import type { Meta, StoryObj } from "@storybook/react-native"
+
+import type { UserPatternWeights } from "@touchgrass/types"
+import { PATTERN_TYPES } from "@touchgrass/types/constants"
+
+import { TopPatternsSection } from "./top-patterns-section"
+
+function makeWeights(
+ overrides: Partial,
+): UserPatternWeights {
+ const base: Partial = {}
+ for (const p of PATTERN_TYPES) base[p.id] = 0.1
+ return { ...base, ...overrides } as UserPatternWeights
+}
+
+const meta = {
+ title: "Patterns/TopPatternsSection",
+ component: TopPatternsSection,
+ args: {
+ patternWeights: makeWeights({
+ "4-HH": 0.95, // Enchanting Visionary
+ "9-HH": 0.85, // Enlightened Traditionalist
+ "1-HH": 0.75, // Personable
+ }),
+ },
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {}
+
+export const CloseMatches: Story = {
+ args: {
+ patternWeights: makeWeights({
+ "2-HH": 0.62, // Enterprising
+ "3-HH": 0.6, // Inquisitive-Achiever
+ "5-HH": 0.58, // Compassionate-Idealist
+ }),
+ },
+}
+
+export const LowConfidence: Story = {
+ args: {
+ patternWeights: makeWeights({
+ "1-LL": 0.22, // Independent-Distant
+ "7-LL": 0.2, // tied lower group
+ "10-LL": 0.18,
+ }),
+ },
+}
diff --git a/apps/mobile/components/patterns/top-patterns-section.tsx b/apps/mobile/components/patterns/top-patterns-section.tsx
index 037fc35..3a48cb1 100644
--- a/apps/mobile/components/patterns/top-patterns-section.tsx
+++ b/apps/mobile/components/patterns/top-patterns-section.tsx
@@ -1,6 +1,7 @@
-import { useEffect, useMemo, useState } from "react"
+import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Pressable, Text, View } from "react-native"
import Animated, {
+ Easing,
useAnimatedStyle,
useSharedValue,
withTiming,
@@ -22,6 +23,13 @@ const PATTERN_BY_ID: Record = Object.fromEntries(
)
const ANIMATION_DURATION = 220
+const PANEL_GAP = 16
+const DETAIL_PADDING = 16
+
+const TIMING_CONFIG = {
+ duration: ANIMATION_DURATION,
+ easing: Easing.out(Easing.cubic),
+}
type Props = {
patternWeights: UserPatternWeights
@@ -37,16 +45,32 @@ function pickTopThree(weights: UserPatternWeights): PatternTypeId[] {
.map(([id]) => id)
}
+function PatternDetail({ pattern }: { pattern: PatternType }) {
+ return (
+ <>
+
+ {pattern.name}
+
+
+ {pattern.shortDescription}
+
+ >
+ )
+}
+
export function TopPatternsSection({ patternWeights }: Props) {
const topThree = useMemo(() => pickTopThree(patternWeights), [patternWeights])
const [selectedId, setSelectedId] = useState(null)
// Keep the last-shown pattern mounted so the collapse animation has
// something to fade/shrink instead of the content vanishing instantly.
const [displayedId, setDisplayedId] = useState(null)
- const [contentHeight, setContentHeight] = useState(0)
+ const heightById = useRef>>({})
const heightValue = useSharedValue(0)
const opacityValue = useSharedValue(0)
+ const gapValue = useSharedValue(0)
const displayed = displayedId !== null ? PATTERN_BY_ID[displayedId] : null
+ const needsMeasure =
+ displayedId !== null && heightById.current[displayedId] === undefined
useEffect(() => {
if (selectedId !== null) {
@@ -59,25 +83,51 @@ export function TopPatternsSection({ patternWeights }: Props) {
return () => clearTimeout(timeout)
}, [selectedId])
+ // Open (cached path): when displayedId changes and the height is already
+ // known, start the animation immediately without going through a state
+ // update. Uncached patterns are handled in handleLayout instead.
useEffect(() => {
- const isOpen = selectedId !== null
- heightValue.value = withTiming(isOpen ? contentHeight : 0, {
- duration: ANIMATION_DURATION,
- })
- opacityValue.value = withTiming(isOpen ? 1 : 0, {
- duration: ANIMATION_DURATION,
- })
- }, [selectedId, contentHeight, heightValue, opacityValue])
+ if (!displayedId) return
+ const cached = heightById.current[displayedId]
+ if (cached === undefined) return
+ heightValue.value = withTiming(cached, TIMING_CONFIG)
+ opacityValue.value = withTiming(1, TIMING_CONFIG)
+ gapValue.value = withTiming(PANEL_GAP, TIMING_CONFIG)
+ }, [displayedId, heightValue, opacityValue, gapValue])
- const animatedStyle = useAnimatedStyle(() => ({
+ // Close: animate to zero when deselected.
+ useEffect(() => {
+ if (selectedId !== null) return
+ heightValue.value = withTiming(0, TIMING_CONFIG)
+ opacityValue.value = withTiming(0, TIMING_CONFIG)
+ gapValue.value = withTiming(0, TIMING_CONFIG)
+ }, [selectedId, heightValue, opacityValue, gapValue])
+
+ const gapStyle = useAnimatedStyle(() => ({
+ height: gapValue.value,
+ }))
+
+ const panelStyle = useAnimatedStyle(() => ({
height: heightValue.value,
opacity: opacityValue.value,
}))
- function handleSelect(id: PatternTypeId) {
- setSelectedId((prev) => (prev === id ? null : id))
+ function handleLayout(height: number) {
+ if (!displayedId || height === 0) return
+ const prev = heightById.current[displayedId]
+ if (prev === height) return
+ heightById.current[displayedId] = height
+ // Trigger animation directly — skips the setContentHeight → re-render →
+ // effect chain that causes visible jank on Android.
+ heightValue.value = withTiming(height, TIMING_CONFIG)
+ opacityValue.value = withTiming(1, TIMING_CONFIG)
+ gapValue.value = withTiming(PANEL_GAP, TIMING_CONFIG)
}
+ const handleSelect = useCallback((id: PatternTypeId) => {
+ setSelectedId((prev) => (prev === id ? null : id))
+ }, [])
+
return (
@@ -112,31 +162,40 @@ export function TopPatternsSection({ patternWeights }: Props) {
)
})}
+
+
+
+ {needsMeasure && displayed ? (
+
+ handleLayout(e.nativeEvent.layout.height)}
+ >
+
+
+
+ ) : null}
+
- setContentHeight(e.nativeEvent.layout.height)}
- style={{ padding: 16 }}
- >
- {displayed ? (
- <>
-
- {displayed.name}
-
-
- {displayed.shortDescription}
-
- >
- ) : null}
+
+ {displayed ? : null}
diff --git a/apps/mobile/components/ui/portable-text.stories.tsx b/apps/mobile/components/ui/portable-text.stories.tsx
new file mode 100644
index 0000000..e8f3e22
--- /dev/null
+++ b/apps/mobile/components/ui/portable-text.stories.tsx
@@ -0,0 +1,71 @@
+import type { PortableTextBlock } from "@portabletext/types"
+import type { Meta, StoryObj } from "@storybook/react-native"
+
+import { PortableText } from "./portable-text"
+
+function paragraph(
+ key: string,
+ spans: { text: string; marks?: string[] }[],
+): PortableTextBlock {
+ return {
+ _type: "block",
+ _key: key,
+ children: spans.map((span, idx) => ({
+ _type: "span",
+ _key: `${key}-${idx}`,
+ text: span.text,
+ marks: span.marks ?? [],
+ })),
+ } as PortableTextBlock
+}
+
+const blocks: PortableTextBlock[] = [
+ paragraph("a", [
+ { text: "Sourdough baking " },
+ { text: "rewards patience", marks: ["strong"] },
+ { text: " more than precision. Start with a healthy starter and a " },
+ { text: "long, slow ferment", marks: ["em"] },
+ { text: "." },
+ ]),
+ paragraph("b", [
+ {
+ text: "Your first loaf will not be perfect, and that is exactly the point — each bake teaches you something about timing and feel.",
+ },
+ ]),
+]
+
+const meta = {
+ title: "UI/PortableText",
+ component: PortableText,
+ args: {
+ blocks,
+ },
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {}
+
+export const SingleParagraph: Story = {
+ args: {
+ blocks: [
+ paragraph("only", [
+ { text: "A short, single paragraph of body copy." },
+ ]),
+ ],
+ },
+}
+
+export const DarkerText: Story = {
+ args: {
+ paragraphClassName: "mb-4 leading-relaxed text-gray-900",
+ },
+}
+
+export const Empty: Story = {
+ args: {
+ blocks: [],
+ },
+}
diff --git a/apps/mobile/lib/auth/errors.ts b/apps/mobile/lib/auth/errors.ts
new file mode 100644
index 0000000..6b8902b
--- /dev/null
+++ b/apps/mobile/lib/auth/errors.ts
@@ -0,0 +1,6 @@
+export class UnauthenticatedError extends Error {
+ constructor() {
+ super("Not authenticated")
+ this.name = "UnauthenticatedError"
+ }
+}
diff --git a/apps/mobile/lib/patterns/api.ts b/apps/mobile/lib/patterns/api.ts
index 7570ccf..bbd889f 100644
--- a/apps/mobile/lib/patterns/api.ts
+++ b/apps/mobile/lib/patterns/api.ts
@@ -1,5 +1,6 @@
import type { PatternWeightsResponse } from "@touchgrass/types"
+import { UnauthenticatedError } from "@/lib/auth/errors"
import { authedFetch } from "@/lib/auth/fetch"
import { apiUrl } from "@/lib/config"
@@ -10,6 +11,7 @@ export async function fetchPatternWeights(): Promise<
> {
const response = await authedFetch(apiUrl("/pattern-weights"))
+ if (response.status === 401) throw new UnauthenticatedError()
if (!response.ok) {
throw new Error(`Failed to fetch pattern weights (${response.status})`)
}
diff --git a/apps/mobile/lib/patterns/use-pattern-weights.ts b/apps/mobile/lib/patterns/use-pattern-weights.ts
index 66c2a87..cad0bda 100644
--- a/apps/mobile/lib/patterns/use-pattern-weights.ts
+++ b/apps/mobile/lib/patterns/use-pattern-weights.ts
@@ -1,7 +1,9 @@
+import { router } from "expo-router"
import { useEffect, useState } from "react"
import type { UserPatternWeights } from "@touchgrass/types"
+import { UnauthenticatedError } from "@/lib/auth/errors"
import { fetchPatternWeights } from "./api"
import { getCachedPatternWeights } from "./cache"
@@ -34,8 +36,12 @@ export function usePatternWeights(): UsePatternWeightsResult {
setWeights(w)
setStatus("ready")
})
- .catch(() => {
+ .catch((err) => {
if (cancelled) return
+ if (err instanceof UnauthenticatedError) {
+ router.replace("/sign-in")
+ return
+ }
setStatus("error")
})
return () => {
diff --git a/apps/mobile/lib/recommendations/api.ts b/apps/mobile/lib/recommendations/api.ts
index e8fbc6c..cbb2ec9 100644
--- a/apps/mobile/lib/recommendations/api.ts
+++ b/apps/mobile/lib/recommendations/api.ts
@@ -1,5 +1,6 @@
import type { Activity, RecommendationsResponse } from "@touchgrass/types"
+import { UnauthenticatedError } from "@/lib/auth/errors"
import { authedFetch } from "@/lib/auth/fetch"
import { apiUrl } from "@/lib/config"
import {
@@ -7,6 +8,8 @@ import {
setCachedPatternWeights,
} from "@/lib/patterns/cache"
+export { UnauthenticatedError } from "@/lib/auth/errors"
+
const activityCache = new Map()
export function getCachedActivity(slug: string): Activity | undefined {
@@ -23,6 +26,10 @@ export class ProfileNotFoundError extends Error {
export async function fetchRecommendations(): Promise {
const response = await authedFetch(apiUrl("/recommendations"))
+ if (response.status === 401) {
+ throw new UnauthenticatedError()
+ }
+
if (response.status === 404) {
throw new ProfileNotFoundError()
}
@@ -51,6 +58,7 @@ export async function fetchActivityBySlug(slug: string): Promise Rule.required(),
}),
defineField({
name: 'tips',
diff --git a/packages/core/src/app.test.ts b/packages/core/src/app.test.ts
index cd2195e..4df7ca7 100644
--- a/packages/core/src/app.test.ts
+++ b/packages/core/src/app.test.ts
@@ -121,3 +121,49 @@ test("unknown routes return a JSON 404", async () => {
const body = (await res.json()) as { error: string }
assert.equal(body.error, "Not found")
})
+
+test("auth: cookie POST with untrusted Origin is rejected before reaching the DB", async () => {
+ const res = await fetch(`${baseUrl}/api/auth/sign-in/email`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Origin: "https://evil.com",
+ },
+ body: JSON.stringify({
+ email: "anyone@example.com",
+ password: "irrelevant-since-we-should-never-get-here",
+ }),
+ })
+
+ // 401 would mean the credential path ran — we want a hard rejection before that.
+ assert.ok(
+ res.status >= 400 && res.status < 500 && res.status !== 401,
+ `expected origin rejection (4xx, not 401), got ${res.status}`,
+ )
+
+ assert.equal(
+ res.headers.get("set-cookie"),
+ null,
+ "no Set-Cookie should be emitted on untrusted-origin POST",
+ )
+})
+
+test("auth: cookie POST with trusted Origin is not blocked at the origin layer", async () => {
+ const res = await fetch(`${baseUrl}/api/auth/sign-in/email`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Origin: "http://localhost:8081",
+ },
+ body: JSON.stringify({
+ email: "anyone@example.com",
+ password: "irrelevant",
+ }),
+ })
+
+ assert.notEqual(
+ res.status,
+ 403,
+ "trusted origin should not be blocked by the origin check",
+ )
+})
diff --git a/packages/core/src/recommendations/sanity-source.ts b/packages/core/src/recommendations/sanity-source.ts
index e61f036..953a7fb 100644
--- a/packages/core/src/recommendations/sanity-source.ts
+++ b/packages/core/src/recommendations/sanity-source.ts
@@ -13,7 +13,7 @@ export type SanityClientConfig = {
const ACTIVITIES_QUERY = `*[_type == "activity" && defined(slug.current) && defined(title)]{
"slug": slug.current,
title,
- "imageUrl": imageUrl.asset->url,
+ "imageUrl": imageUrl.asset->url + "?w=800&fit=crop&auto=format",
type,
field,
estimated_time,
diff --git a/packages/mocks/recommendations.ts b/packages/mocks/recommendations.ts
index 884ab00..515aef6 100644
--- a/packages/mocks/recommendations.ts
+++ b/packages/mocks/recommendations.ts
@@ -15,7 +15,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-guitar-pedalboard",
title: "Build a Guitar Pedalboard",
- imageUrl: "https://images.unsplash.com/photo-1511379938547-c1f69419868d",
+ imageUrl: "https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Music",
estimated_time: "A weekend",
@@ -30,7 +30,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "join-a-beginner-boxing-class",
title: "Join a Beginner Boxing Class",
- imageUrl: "https://images.unsplash.com/photo-1517836357463-d25dfeac3438",
+ imageUrl: "https://images.unsplash.com/photo-1517836357463-d25dfeac3438?w=800&fit=crop&auto=format",
type: "Active",
field: "Martial Arts",
estimated_time: "1 hour",
@@ -45,7 +45,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "write-a-short-horror-screenplay",
title: "Write a Short Horror Screenplay",
- imageUrl: "https://images.unsplash.com/photo-1489599849927-2ee91cede3ba",
+ imageUrl: "https://images.unsplash.com/photo-1489599849927-2ee91cede3ba?w=800&fit=crop&auto=format",
type: "Artistic",
field: "Writing",
estimated_time: "A few hours",
@@ -60,7 +60,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "journal-while-listening-to-jazz",
title: "Journal While Listening to Jazz",
- imageUrl: "https://images.unsplash.com/photo-1511192336575-5a79af67a629",
+ imageUrl: "https://images.unsplash.com/photo-1511192336575-5a79af67a629?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Writing",
estimated_time: "1 hour",
@@ -75,7 +75,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "study-music-theory-basics",
title: "Study Music Theory Basics",
- imageUrl: "https://images.unsplash.com/photo-1514119412350-e174d90d280e",
+ imageUrl: "https://images.unsplash.com/photo-1514119412350-e174d90d280e?w=800&fit=crop&auto=format",
type: "Educational",
field: "Music",
estimated_time: "A week",
@@ -90,7 +90,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "compose-a-1-minute-song",
title: "Compose a 1-Minute Song",
- imageUrl: "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f",
+ imageUrl: "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=800&fit=crop&auto=format",
type: "Creative",
field: "Music",
estimated_time: "A few hours",
@@ -105,7 +105,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "host-a-vinyl-listening-night",
title: "Host a Vinyl Listening Night",
- imageUrl: "https://images.unsplash.com/photo-1460661419201-fd4cecdf8a8b",
+ imageUrl: "https://images.unsplash.com/photo-1460661419201-fd4cecdf8a8b?w=800&fit=crop&auto=format",
type: "Social",
field: "Music",
estimated_time: "An evening",
@@ -120,7 +120,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-home-heavy-bag-stand",
title: "Build a Home Heavy Bag Stand",
- imageUrl: "https://images.unsplash.com/photo-1517838277536-f5f99be501cd",
+ imageUrl: "https://images.unsplash.com/photo-1517838277536-f5f99be501cd?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Martial Arts",
estimated_time: "A weekend",
@@ -135,7 +135,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "learn-tai-chi-basics",
title: "Learn Tai Chi Basics",
- imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773",
+ imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773?w=800&fit=crop&auto=format",
type: "Mindful",
field: "Martial Arts",
estimated_time: "A week",
@@ -150,7 +150,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "read-a-bruce-lee-biography",
title: "Read a Bruce Lee Biography",
- imageUrl: "https://images.unsplash.com/photo-1544717305-2782549b5136",
+ imageUrl: "https://images.unsplash.com/photo-1544717305-2782549b5136?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Martial Arts",
estimated_time: "A week",
@@ -165,7 +165,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "plan-a-martial-arts-training-trip",
title: "Plan a Martial Arts Training Trip",
- imageUrl: "https://images.unsplash.com/photo-1544717684-1243da23b545",
+ imageUrl: "https://images.unsplash.com/photo-1544717684-1243da23b545?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Martial Arts",
estimated_time: "A weekend",
@@ -180,7 +180,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "start-a-30-day-reading-challenge",
title: "Start a 30-Day Reading Challenge",
- imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794",
+ imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Writing",
estimated_time: "A month",
@@ -195,7 +195,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "join-a-local-book-club",
title: "Join a Local Book Club",
- imageUrl: "https://images.unsplash.com/photo-1521587760476-6c12a4b040da",
+ imageUrl: "https://images.unsplash.com/photo-1521587760476-6c12a4b040da?w=800&fit=crop&auto=format",
type: "Social",
field: "Writing",
estimated_time: "Ongoing",
@@ -210,7 +210,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "submit-a-story-to-a-literary-magazine",
title: "Submit a Story to a Literary Magazine",
- imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a",
+ imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a?w=800&fit=crop&auto=format",
type: "Professional",
field: "Writing",
estimated_time: "A weekend",
@@ -225,7 +225,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "analyze-a-classic-novel-chapter-by-chapter",
title: "Analyze a Classic Novel Chapter by Chapter",
- imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794",
+ imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Writing",
estimated_time: "A week",
@@ -240,7 +240,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "develop-a-personal-cookbook",
title: "Develop a Personal Cookbook",
- imageUrl: "https://images.unsplash.com/photo-1490645935967-10de6ba17061",
+ imageUrl: "https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=800&fit=crop&auto=format",
type: "Creative",
field: "Cooking",
estimated_time: "A month",
@@ -255,7 +255,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "cook-one-regions-cuisine-for-a-week",
title: "Cook One Region's Cuisine for a Week",
- imageUrl: "https://images.unsplash.com/photo-1504674900247-0877df9cc836",
+ imageUrl: "https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Cooking",
estimated_time: "A week",
@@ -270,7 +270,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "pitch-a-pop-up-restaurant-concept",
title: "Pitch a Pop-Up Restaurant Concept",
- imageUrl: "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4",
+ imageUrl: "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&fit=crop&auto=format",
type: "Professional",
field: "Cooking",
estimated_time: "A week",
@@ -285,7 +285,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "shoot-a-golden-hour-walk",
title: "Shoot a Golden Hour Walk",
- imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
+ imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Photography",
estimated_time: "1 hour",
@@ -300,7 +300,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-working-pinhole-camera",
title: "Build a Working Pinhole Camera",
- imageUrl: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32",
+ imageUrl: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Photography",
estimated_time: "A weekend",
@@ -315,7 +315,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "create-a-12-page-photo-zine",
title: "Create a 12-Page Photo Zine",
- imageUrl: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32",
+ imageUrl: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?w=800&fit=crop&auto=format",
type: "Artistic",
field: "Photography",
estimated_time: "A weekend",
@@ -330,7 +330,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "lead-a-saturday-photowalk",
title: "Lead a Saturday Photowalk",
- imageUrl: "https://images.unsplash.com/photo-1492691527719-9d1e07e534b4",
+ imageUrl: "https://images.unsplash.com/photo-1492691527719-9d1e07e534b4?w=800&fit=crop&auto=format",
type: "Social",
field: "Photography",
estimated_time: "An afternoon",
@@ -345,7 +345,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "capture-wildlife-in-a-state-park",
title: "Capture Wildlife in a State Park",
- imageUrl: "https://images.unsplash.com/photo-1474511320723-9a56873867b5",
+ imageUrl: "https://images.unsplash.com/photo-1474511320723-9a56873867b5?w=800&fit=crop&auto=format",
type: "Outdoorsy",
field: "Photography",
estimated_time: "An afternoon",
@@ -360,7 +360,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "run-a-tabletop-rpg-one-shot",
title: "Run a Tabletop RPG One-Shot",
- imageUrl: "https://images.unsplash.com/photo-1511512578047-dfb367046420",
+ imageUrl: "https://images.unsplash.com/photo-1511512578047-dfb367046420?w=800&fit=crop&auto=format",
type: "Social",
field: "Gaming",
estimated_time: "An afternoon",
@@ -375,7 +375,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "design-a-board-game-prototype",
title: "Design a Board Game Prototype",
- imageUrl: "https://images.unsplash.com/photo-1610890716171-6b1bb98ffd09",
+ imageUrl: "https://images.unsplash.com/photo-1610890716171-6b1bb98ffd09?w=800&fit=crop&auto=format",
type: "Creative",
field: "Gaming",
estimated_time: "A weekend",
@@ -390,7 +390,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "stream-your-first-twitch-session",
title: "Stream Your First Twitch Session",
- imageUrl: "https://images.unsplash.com/photo-1542751371-adc38448a05e",
+ imageUrl: "https://images.unsplash.com/photo-1542751371-adc38448a05e?w=800&fit=crop&auto=format",
type: "Professional",
field: "Gaming",
estimated_time: "1 hour",
@@ -405,7 +405,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "beat-a-notoriously-difficult-rpg",
title: "Beat a Notoriously Difficult RPG",
- imageUrl: "https://images.unsplash.com/photo-1511512578047-dfb367046420",
+ imageUrl: "https://images.unsplash.com/photo-1511512578047-dfb367046420?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Gaming",
estimated_time: "A month",
@@ -420,7 +420,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "try-a-30-day-push-up-challenge",
title: "Try a 30-Day Push-Up Challenge",
- imageUrl: "https://images.unsplash.com/photo-1517836357463-d25dfeac3438",
+ imageUrl: "https://images.unsplash.com/photo-1517836357463-d25dfeac3438?w=800&fit=crop&auto=format",
type: "Active",
field: "Fitness",
estimated_time: "A month",
@@ -435,7 +435,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-garage-gym-setup",
title: "Build a Garage Gym Setup",
- imageUrl: "https://images.unsplash.com/photo-1534438327276-14e5300c3a48",
+ imageUrl: "https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Fitness",
estimated_time: "A weekend",
@@ -450,7 +450,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "map-a-park-workout-loop",
title: "Map a Park Workout Loop",
- imageUrl: "https://images.unsplash.com/photo-1506744038136-46273834b3fb",
+ imageUrl: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&fit=crop&auto=format",
type: "Outdoorsy",
field: "Fitness",
estimated_time: "An afternoon",
@@ -465,7 +465,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "sign-up-for-your-first-5k",
title: "Sign Up for Your First 5K",
- imageUrl: "https://images.unsplash.com/photo-1483721310020-03333e577078",
+ imageUrl: "https://images.unsplash.com/photo-1483721310020-03333e577078?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Fitness",
estimated_time: "A month",
@@ -480,7 +480,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "get-personal-trainer-certified",
title: "Get Personal Trainer Certified",
- imageUrl: "https://images.unsplash.com/photo-1517836357463-d25dfeac3438",
+ imageUrl: "https://images.unsplash.com/photo-1517836357463-d25dfeac3438?w=800&fit=crop&auto=format",
type: "Professional",
field: "Fitness",
estimated_time: "A month",
@@ -495,7 +495,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-personal-site-from-scratch",
title: "Build a Personal Site From Scratch",
- imageUrl: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6",
+ imageUrl: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Coding",
estimated_time: "A weekend",
@@ -510,7 +510,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "read-the-pragmatic-programmer",
title: "Read The Pragmatic Programmer",
- imageUrl: "https://images.unsplash.com/photo-1515879218367-8466d910aaa4",
+ imageUrl: "https://images.unsplash.com/photo-1515879218367-8466d910aaa4?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Coding",
estimated_time: "A week",
@@ -525,7 +525,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "join-a-local-hack-night",
title: "Join a Local Hack Night",
- imageUrl: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3",
+ imageUrl: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&fit=crop&auto=format",
type: "Social",
field: "Coding",
estimated_time: "1 hour",
@@ -540,7 +540,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "contribute-your-first-open-source-pr",
title: "Contribute Your First Open Source PR",
- imageUrl: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3",
+ imageUrl: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&fit=crop&auto=format",
type: "Professional",
field: "Coding",
estimated_time: "A week",
@@ -555,7 +555,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "run-a-kitchen-chemistry-experiment",
title: "Run a Kitchen Chemistry Experiment",
- imageUrl: "https://images.unsplash.com/photo-1532187643603-ba119ca4109e",
+ imageUrl: "https://images.unsplash.com/photo-1532187643603-ba119ca4109e?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Science",
estimated_time: "1 hour",
@@ -570,7 +570,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "read-cosmos-by-carl-sagan",
title: "Read Cosmos by Carl Sagan",
- imageUrl: "https://images.unsplash.com/photo-1462331940025-496dfbfc7564",
+ imageUrl: "https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Science",
estimated_time: "A week",
@@ -585,7 +585,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "track-a-citizen-science-project",
title: "Track a Citizen Science Project",
- imageUrl: "https://images.unsplash.com/photo-1532094349884-543bc11b234d",
+ imageUrl: "https://images.unsplash.com/photo-1532094349884-543bc11b234d?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Science",
estimated_time: "A month",
@@ -600,7 +600,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "hike-to-a-geological-formation",
title: "Hike to a Geological Formation",
- imageUrl: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b",
+ imageUrl: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=800&fit=crop&auto=format",
type: "Outdoorsy",
field: "Science",
estimated_time: "An afternoon",
@@ -615,7 +615,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "apply-to-a-lab-volunteer-program",
title: "Apply to a Lab Volunteer Program",
- imageUrl: "https://images.unsplash.com/photo-1532187643603-ba119ca4109e",
+ imageUrl: "https://images.unsplash.com/photo-1532187643603-ba119ca4109e?w=800&fit=crop&auto=format",
type: "Professional",
field: "Science",
estimated_time: "A week",
@@ -630,7 +630,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "start-an-indoor-herb-garden",
title: "Start an Indoor Herb Garden",
- imageUrl: "https://images.unsplash.com/photo-1466692476868-aef1dfb1e735",
+ imageUrl: "https://images.unsplash.com/photo-1466692476868-aef1dfb1e735?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Nature",
estimated_time: "A few hours",
@@ -645,7 +645,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "spend-an-hour-forest-bathing",
title: "Spend an Hour Forest Bathing",
- imageUrl: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e",
+ imageUrl: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Nature",
estimated_time: "1 hour",
@@ -660,7 +660,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "identify-ten-local-plant-species",
title: "Identify Ten Local Plant Species",
- imageUrl: "https://images.unsplash.com/photo-1466692476868-aef1dfb1e735",
+ imageUrl: "https://images.unsplash.com/photo-1466692476868-aef1dfb1e735?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Nature",
estimated_time: "An afternoon",
@@ -675,7 +675,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "volunteer-for-a-trail-cleanup",
title: "Volunteer for a Trail Cleanup",
- imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
+ imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=800&fit=crop&auto=format",
type: "Social",
field: "Nature",
estimated_time: "An afternoon",
@@ -690,7 +690,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "camp-solo-for-a-single-night",
title: "Camp Solo for a Single Night",
- imageUrl: "https://images.unsplash.com/photo-1506744038136-46273834b3fb",
+ imageUrl: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Nature",
estimated_time: "A weekend",
@@ -705,7 +705,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "watch-a-directors-full-filmography",
title: "Watch a Director's Full Filmography",
- imageUrl: "https://images.unsplash.com/photo-1489599849927-2ee91cede3ba",
+ imageUrl: "https://images.unsplash.com/photo-1489599849927-2ee91cede3ba?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Film",
estimated_time: "A month",
@@ -720,7 +720,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "host-a-themed-film-night",
title: "Host a Themed Film Night",
- imageUrl: "https://images.unsplash.com/photo-1489599849927-2ee91cede3ba",
+ imageUrl: "https://images.unsplash.com/photo-1489599849927-2ee91cede3ba?w=800&fit=crop&auto=format",
type: "Social",
field: "Film",
estimated_time: "1 hour",
@@ -735,7 +735,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "pitch-a-tv-pilot-logline",
title: "Pitch a TV Pilot Logline",
- imageUrl: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3",
+ imageUrl: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&fit=crop&auto=format",
type: "Professional",
field: "Film",
estimated_time: "A few hours",
@@ -750,7 +750,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "reflect-on-your-five-favorite-films",
title: "Reflect on Your Five Favorite Films",
- imageUrl: "https://images.unsplash.com/photo-1485846234645-a62644f84728",
+ imageUrl: "https://images.unsplash.com/photo-1485846234645-a62644f84728?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Film",
estimated_time: "1 hour",
@@ -765,7 +765,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "audition-for-community-theater",
title: "Audition for Community Theater",
- imageUrl: "https://images.unsplash.com/photo-1503095396549-807759245b35",
+ imageUrl: "https://images.unsplash.com/photo-1503095396549-807759245b35?w=800&fit=crop&auto=format",
type: "Active",
field: "Theater",
estimated_time: "An afternoon",
@@ -780,7 +780,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-set-piece-for-a-production",
title: "Build a Set Piece for a Production",
- imageUrl: "https://images.unsplash.com/photo-1503095396549-807759245b35",
+ imageUrl: "https://images.unsplash.com/photo-1503095396549-807759245b35?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Theater",
estimated_time: "A weekend",
@@ -795,7 +795,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "write-a-10-minute-play",
title: "Write a 10-Minute Play",
- imageUrl: "https://images.unsplash.com/photo-1516280440614-37939bbacd81",
+ imageUrl: "https://images.unsplash.com/photo-1516280440614-37939bbacd81?w=800&fit=crop&auto=format",
type: "Artistic",
field: "Theater",
estimated_time: "A few hours",
@@ -810,7 +810,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "run-an-improv-night-with-friends",
title: "Run an Improv Night With Friends",
- imageUrl: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac",
+ imageUrl: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=800&fit=crop&auto=format",
type: "Social",
field: "Theater",
estimated_time: "1 hour",
@@ -825,7 +825,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "direct-a-staged-reading",
title: "Direct a Staged Reading",
- imageUrl: "https://images.unsplash.com/photo-1503095396549-807759245b35",
+ imageUrl: "https://images.unsplash.com/photo-1503095396549-807759245b35?w=800&fit=crop&auto=format",
type: "Professional",
field: "Theater",
estimated_time: "A weekend",
@@ -840,7 +840,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "take-a-pottery-wheel-class",
title: "Take a Pottery Wheel Class",
- imageUrl: "https://images.unsplash.com/photo-1515377905703-c4788e51af15",
+ imageUrl: "https://images.unsplash.com/photo-1515377905703-c4788e51af15?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Art",
estimated_time: "1 hour",
@@ -855,7 +855,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "sketch-in-a-public-park",
title: "Sketch in a Public Park",
- imageUrl: "https://images.unsplash.com/photo-1513364776144-60967b0f800f",
+ imageUrl: "https://images.unsplash.com/photo-1513364776144-60967b0f800f?w=800&fit=crop&auto=format",
type: "Outdoorsy",
field: "Art",
estimated_time: "1 hour",
@@ -870,7 +870,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "curate-a-gallery-wall-in-your-home",
title: "Curate a Gallery Wall in Your Home",
- imageUrl: "https://images.unsplash.com/photo-1513694203232-719a280e022f",
+ imageUrl: "https://images.unsplash.com/photo-1513694203232-719a280e022f?w=800&fit=crop&auto=format",
type: "Creative",
field: "Art",
estimated_time: "An afternoon",
@@ -885,7 +885,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "sell-a-piece-at-a-local-market",
title: "Sell a Piece at a Local Market",
- imageUrl: "https://images.unsplash.com/photo-1511578314322-379afb476865",
+ imageUrl: "https://images.unsplash.com/photo-1511578314322-379afb476865?w=800&fit=crop&auto=format",
type: "Professional",
field: "Art",
estimated_time: "A weekend",
@@ -900,7 +900,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "draft-a-personal-essay",
title: "Draft a Personal Essay",
- imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a",
+ imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Writing",
estimated_time: "A few hours",
@@ -915,7 +915,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "co-write-a-story-with-a-friend",
title: "Co-Write a Story With a Friend",
- imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f",
+ imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=800&fit=crop&auto=format",
type: "Social",
field: "Writing",
estimated_time: "A few hours",
@@ -930,7 +930,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "try-stream-of-consciousness-daily",
title: "Try Stream-of-Consciousness Daily",
- imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a",
+ imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a?w=800&fit=crop&auto=format",
type: "Creative",
field: "Writing",
estimated_time: "A month",
@@ -945,7 +945,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "pitch-a-magazine-feature-article",
title: "Pitch a Magazine Feature Article",
- imageUrl: "https://images.unsplash.com/photo-1495020689067-958852a7765e",
+ imageUrl: "https://images.unsplash.com/photo-1495020689067-958852a7765e?w=800&fit=crop&auto=format",
type: "Professional",
field: "Writing",
estimated_time: "A week",
@@ -960,7 +960,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "travel-for-writing-inspiration",
title: "Travel for Writing Inspiration",
- imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
+ imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Writing",
estimated_time: "A weekend",
@@ -975,7 +975,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "take-a-beginner-salsa-class",
title: "Take a Beginner Salsa Class",
- imageUrl: "https://images.unsplash.com/photo-1504609773096-104ff2c73ba4",
+ imageUrl: "https://images.unsplash.com/photo-1504609773096-104ff2c73ba4?w=800&fit=crop&auto=format",
type: "Active",
field: "Dance",
estimated_time: "1 hour",
@@ -990,7 +990,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "learn-a-tiktok-choreography",
title: "Learn a TikTok Choreography",
- imageUrl: "https://images.unsplash.com/photo-1504609773096-104ff2c73ba4",
+ imageUrl: "https://images.unsplash.com/photo-1504609773096-104ff2c73ba4?w=800&fit=crop&auto=format",
type: "Performative",
field: "Dance",
estimated_time: "1 hour",
@@ -1005,7 +1005,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "attend-a-friday-night-dance-social",
title: "Attend a Friday Night Dance Social",
- imageUrl: "https://images.unsplash.com/photo-1492684223066-81342ee5ff30",
+ imageUrl: "https://images.unsplash.com/photo-1492684223066-81342ee5ff30?w=800&fit=crop&auto=format",
type: "Social",
field: "Dance",
estimated_time: "An afternoon",
@@ -1020,7 +1020,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "choreograph-a-30-second-routine",
title: "Choreograph a 30-Second Routine",
- imageUrl: "https://images.unsplash.com/photo-1504609773096-104ff2c73ba4",
+ imageUrl: "https://images.unsplash.com/photo-1504609773096-104ff2c73ba4?w=800&fit=crop&auto=format",
type: "Artistic",
field: "Dance",
estimated_time: "A few hours",
@@ -1035,7 +1035,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "study-the-history-of-modern-dance",
title: "Study the History of Modern Dance",
- imageUrl: "https://images.unsplash.com/photo-1504609773096-104ff2c73ba4",
+ imageUrl: "https://images.unsplash.com/photo-1504609773096-104ff2c73ba4?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Dance",
estimated_time: "A week",
@@ -1050,7 +1050,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "ride-a-new-20-mile-loop",
title: "Ride a New 20-Mile Loop",
- imageUrl: "https://images.unsplash.com/photo-1507035895480-2b3156c31fc8",
+ imageUrl: "https://images.unsplash.com/photo-1507035895480-2b3156c31fc8?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Cycling",
estimated_time: "An afternoon",
@@ -1065,7 +1065,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-up-a-used-bike-frame",
title: "Build Up a Used Bike Frame",
- imageUrl: "https://images.unsplash.com/photo-1517649763962-0c623066013b",
+ imageUrl: "https://images.unsplash.com/photo-1517649763962-0c623066013b?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Cycling",
estimated_time: "A weekend",
@@ -1080,7 +1080,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "join-a-sunday-group-ride",
title: "Join a Sunday Group Ride",
- imageUrl: "https://images.unsplash.com/photo-1517649763962-0c623066013b",
+ imageUrl: "https://images.unsplash.com/photo-1517649763962-0c623066013b?w=800&fit=crop&auto=format",
type: "Social",
field: "Cycling",
estimated_time: "An afternoon",
@@ -1095,7 +1095,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "race-your-first-local-crit",
title: "Race Your First Local Crit",
- imageUrl: "https://images.unsplash.com/photo-1541625602330-2277a4c46182",
+ imageUrl: "https://images.unsplash.com/photo-1541625602330-2277a4c46182?w=800&fit=crop&auto=format",
type: "Active",
field: "Cycling",
estimated_time: "1 hour",
@@ -1110,7 +1110,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "cycle-through-a-national-park",
title: "Cycle Through a National Park",
- imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
+ imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=800&fit=crop&auto=format",
type: "Outdoorsy",
field: "Cycling",
estimated_time: "A weekend",
@@ -1125,7 +1125,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "hike-a-sunrise-summit",
title: "Hike a Sunrise Summit",
- imageUrl: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b",
+ imageUrl: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Hiking",
estimated_time: "An afternoon",
@@ -1140,7 +1140,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "plan-a-three-day-backpacking-trip",
title: "Plan a Three-Day Backpacking Trip",
- imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
+ imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=800&fit=crop&auto=format",
type: "Outdoorsy",
field: "Hiking",
estimated_time: "A weekend",
@@ -1155,7 +1155,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "lead-a-friend-on-their-first-hike",
title: "Lead a Friend on Their First Hike",
- imageUrl: "https://images.unsplash.com/photo-1551632811-561732d1e306",
+ imageUrl: "https://images.unsplash.com/photo-1551632811-561732d1e306?w=800&fit=crop&auto=format",
type: "Social",
field: "Hiking",
estimated_time: "An afternoon",
@@ -1170,7 +1170,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "photograph-a-trail-in-a-series",
title: "Photograph a Trail in a Series",
- imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
+ imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=800&fit=crop&auto=format",
type: "Artistic",
field: "Hiking",
estimated_time: "An afternoon",
@@ -1185,7 +1185,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "journal-at-a-mountain-overlook",
title: "Journal at a Mountain Overlook",
- imageUrl: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b",
+ imageUrl: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Hiking",
estimated_time: "1 hour",
@@ -1200,7 +1200,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "plan-a-weekend-solo-trip",
title: "Plan a Weekend Solo Trip",
- imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
+ imageUrl: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Travel",
estimated_time: "A weekend",
@@ -1215,7 +1215,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-travel-photo-book",
title: "Build a Travel Photo Book",
- imageUrl: "https://images.unsplash.com/photo-1488646953014-85cb44e25828",
+ imageUrl: "https://images.unsplash.com/photo-1488646953014-85cb44e25828?w=800&fit=crop&auto=format",
type: "Creative",
field: "Travel",
estimated_time: "A weekend",
@@ -1230,7 +1230,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "learn-50-words-in-a-new-language",
title: "Learn 50 Words in a New Language",
- imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f",
+ imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Travel",
estimated_time: "A week",
@@ -1245,7 +1245,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "host-a-couchsurfer-for-a-weekend",
title: "Host a Couchsurfer for a Weekend",
- imageUrl: "https://images.unsplash.com/photo-1522708323590-d24dbb6b0267",
+ imageUrl: "https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800&fit=crop&auto=format",
type: "Social",
field: "Travel",
estimated_time: "A weekend",
@@ -1260,7 +1260,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "apply-to-be-a-travel-writer",
title: "Apply to Be a Travel Writer",
- imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a",
+ imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a?w=800&fit=crop&auto=format",
type: "Professional",
field: "Travel",
estimated_time: "A week",
@@ -1275,7 +1275,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-morning-routine",
title: "Build a Morning Routine",
- imageUrl: "https://images.unsplash.com/photo-1499209974431-9dddcece7f88",
+ imageUrl: "https://images.unsplash.com/photo-1499209974431-9dddcece7f88?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Wellness",
estimated_time: "A month",
@@ -1290,7 +1290,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "take-a-yin-yoga-class",
title: "Take a Yin Yoga Class",
- imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773",
+ imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773?w=800&fit=crop&auto=format",
type: "Active",
field: "Wellness",
estimated_time: "1 hour",
@@ -1305,7 +1305,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "track-your-sleep-for-two-weeks",
title: "Track Your Sleep for Two Weeks",
- imageUrl: "https://images.unsplash.com/photo-1495195134817-aeb325a55b65",
+ imageUrl: "https://images.unsplash.com/photo-1495195134817-aeb325a55b65?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Wellness",
estimated_time: "A week",
@@ -1320,7 +1320,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "become-a-certified-yoga-teacher",
title: "Become a Certified Yoga Teacher",
- imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773",
+ imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773?w=800&fit=crop&auto=format",
type: "Professional",
field: "Wellness",
estimated_time: "A month",
@@ -1335,7 +1335,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-backyard-telescope-mount",
title: "Build a Backyard Telescope Mount",
- imageUrl: "https://images.unsplash.com/photo-1462331940025-496dfbfc7564",
+ imageUrl: "https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Astronomy",
estimated_time: "A weekend",
@@ -1350,7 +1350,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "reflect-on-the-cosmos-under-the-night-sky",
title: "Reflect on the Cosmos Under the Night Sky",
- imageUrl: "https://images.unsplash.com/photo-1462331940025-496dfbfc7564",
+ imageUrl: "https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Astronomy",
estimated_time: "1 hour",
@@ -1365,7 +1365,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "photograph-the-milky-way",
title: "Photograph the Milky Way",
- imageUrl: "https://images.unsplash.com/photo-1502134249126-9f3755a50d78",
+ imageUrl: "https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800&fit=crop&auto=format",
type: "Artistic",
field: "Astronomy",
estimated_time: "An afternoon",
@@ -1380,7 +1380,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "follow-a-tech-deep-dive-channel-for-a-month",
title: "Follow a Tech Deep-Dive Channel for a Month",
- imageUrl: "https://images.unsplash.com/photo-1518770660439-4636190af475",
+ imageUrl: "https://images.unsplash.com/photo-1518770660439-4636190af475?w=800&fit=crop&auto=format",
type: "Educational",
field: "Technology",
estimated_time: "A month",
@@ -1395,7 +1395,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "3d-print-a-custom-phone-stand",
title: "3D Print a Custom Phone Stand",
- imageUrl: "https://images.unsplash.com/photo-1515879218367-8466d910aaa4",
+ imageUrl: "https://images.unsplash.com/photo-1515879218367-8466d910aaa4?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Engineering",
estimated_time: "A weekend",
@@ -1410,7 +1410,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-simple-arduino-robot",
title: "Build a Simple Arduino Robot",
- imageUrl: "https://images.unsplash.com/photo-1518770660439-4636190af475",
+ imageUrl: "https://images.unsplash.com/photo-1518770660439-4636190af475?w=800&fit=crop&auto=format",
type: "Experimental",
field: "Engineering",
estimated_time: "A weekend",
@@ -1425,7 +1425,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "detail-your-car-to-showroom-standard",
title: "Detail Your Car to Showroom Standard",
- imageUrl: "https://images.unsplash.com/photo-1503376780353-7e6692767b70",
+ imageUrl: "https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Cars",
estimated_time: "An afternoon",
@@ -1440,7 +1440,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "attend-a-weekend-track-day",
title: "Attend a Weekend Track Day",
- imageUrl: "https://images.unsplash.com/photo-1502877338535-766e1452684a",
+ imageUrl: "https://images.unsplash.com/photo-1502877338535-766e1452684a?w=800&fit=crop&auto=format",
type: "Active",
field: "Cars",
estimated_time: "A weekend",
@@ -1455,7 +1455,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "get-your-motorcycle-license",
title: "Get Your Motorcycle License",
- imageUrl: "https://images.unsplash.com/photo-1517846693594-1567da72af75",
+ imageUrl: "https://images.unsplash.com/photo-1517846693594-1567da72af75?w=800&fit=crop&auto=format",
type: "Active",
field: "Motorcycles",
estimated_time: "A month",
@@ -1470,7 +1470,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "plan-a-coastal-motorcycle-road-trip",
title: "Plan a Coastal Motorcycle Road Trip",
- imageUrl: "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429",
+ imageUrl: "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Motorcycles",
estimated_time: "A weekend",
@@ -1485,7 +1485,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "take-an-intro-flight-lesson",
title: "Take an Intro Flight Lesson",
- imageUrl: "https://images.unsplash.com/photo-1436491865332-7a61a109cc05",
+ imageUrl: "https://images.unsplash.com/photo-1436491865332-7a61a109cc05?w=800&fit=crop&auto=format",
type: "Active",
field: "Aviation",
estimated_time: "1 hour",
@@ -1500,7 +1500,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-remote-control-plane",
title: "Build a Remote-Control Plane",
- imageUrl: "https://images.unsplash.com/photo-1474302770737-173ee21bab63",
+ imageUrl: "https://images.unsplash.com/photo-1474302770737-173ee21bab63?w=800&fit=crop&auto=format",
type: "Constructive",
field: "Aviation",
estimated_time: "A weekend",
@@ -1515,7 +1515,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "join-a-recreational-soccer-league",
title: "Join a Recreational Soccer League",
- imageUrl: "https://images.unsplash.com/photo-1517466787929-bc90951d0974",
+ imageUrl: "https://images.unsplash.com/photo-1517466787929-bc90951d0974?w=800&fit=crop&auto=format",
type: "Social",
field: "Sports",
estimated_time: "Ongoing",
@@ -1530,7 +1530,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "attend-a-live-sporting-event",
title: "Attend a Live Sporting Event",
- imageUrl: "https://images.unsplash.com/photo-1508098682722-e99c643e7485",
+ imageUrl: "https://images.unsplash.com/photo-1508098682722-e99c643e7485?w=800&fit=crop&auto=format",
type: "Social",
field: "Sports",
estimated_time: "An afternoon",
@@ -1545,7 +1545,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "join-a-local-running-club",
title: "Join a Local Running Club",
- imageUrl: "https://images.unsplash.com/photo-1483721310020-03333e577078",
+ imageUrl: "https://images.unsplash.com/photo-1483721310020-03333e577078?w=800&fit=crop&auto=format",
type: "Social",
field: "Running",
estimated_time: "Ongoing",
@@ -1560,7 +1560,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "try-an-indoor-bouldering-gym",
title: "Try an Indoor Bouldering Gym",
- imageUrl: "https://images.unsplash.com/photo-1522163182402-834f871fd851",
+ imageUrl: "https://images.unsplash.com/photo-1522163182402-834f871fd851?w=800&fit=crop&auto=format",
type: "Active",
field: "Climbing",
estimated_time: "1 hour",
@@ -1575,7 +1575,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "take-an-outdoor-rock-climbing-course",
title: "Take an Outdoor Rock Climbing Course",
- imageUrl: "https://images.unsplash.com/photo-1522163182402-834f871fd851",
+ imageUrl: "https://images.unsplash.com/photo-1522163182402-834f871fd851?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Climbing",
estimated_time: "A weekend",
@@ -1590,7 +1590,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "host-a-monthly-board-game-night",
title: "Host a Monthly Board Game Night",
- imageUrl: "https://images.unsplash.com/photo-1610890716171-6b1bb98ffd09",
+ imageUrl: "https://images.unsplash.com/photo-1610890716171-6b1bb98ffd09?w=800&fit=crop&auto=format",
type: "Social",
field: "Board Games",
estimated_time: "An afternoon",
@@ -1605,7 +1605,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "solo-through-a-complex-strategy-game",
title: "Solo Through a Complex Strategy Game",
- imageUrl: "https://images.unsplash.com/photo-1610890716171-6b1bb98ffd09",
+ imageUrl: "https://images.unsplash.com/photo-1610890716171-6b1bb98ffd09?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Board Games",
estimated_time: "An afternoon",
@@ -1620,7 +1620,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "visit-five-local-independent-cafes",
title: "Visit Five Local Independent Cafes",
- imageUrl: "https://images.unsplash.com/photo-1495474472287-4d71bcdd2085",
+ imageUrl: "https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=800&fit=crop&auto=format",
type: "Exploratory",
field: "Coffee",
estimated_time: "A month",
@@ -1635,7 +1635,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "curate-a-capsule-wardrobe",
title: "Curate a Capsule Wardrobe",
- imageUrl: "https://images.unsplash.com/photo-1496747611176-843222e1e57c",
+ imageUrl: "https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=800&fit=crop&auto=format",
type: "Creative",
field: "Fashion",
estimated_time: "An afternoon",
@@ -1650,7 +1650,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "thrift-a-full-outfit-under-20",
title: "Thrift a Full Outfit Under $20",
- imageUrl: "https://images.unsplash.com/photo-1483985988355-763728e1935b",
+ imageUrl: "https://images.unsplash.com/photo-1483985988355-763728e1935b?w=800&fit=crop&auto=format",
type: "Adventurous",
field: "Fashion",
estimated_time: "An afternoon",
@@ -1665,7 +1665,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "car-camp-at-a-state-park",
title: "Car Camp at a State Park",
- imageUrl: "https://images.unsplash.com/photo-1506744038136-46273834b3fb",
+ imageUrl: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&fit=crop&auto=format",
type: "Outdoorsy",
field: "Camping",
estimated_time: "A weekend",
@@ -1680,7 +1680,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "master-three-essential-wilderness-skills",
title: "Master Three Essential Wilderness Skills",
- imageUrl: "https://images.unsplash.com/photo-1472396961693-142e6e269027",
+ imageUrl: "https://images.unsplash.com/photo-1472396961693-142e6e269027?w=800&fit=crop&auto=format",
type: "Skill-based",
field: "Camping",
estimated_time: "A weekend",
@@ -1695,7 +1695,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "read-thinking-fast-and-slow",
title: "Read Thinking, Fast and Slow",
- imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794",
+ imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Psychology",
estimated_time: "A week",
@@ -1710,7 +1710,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "try-cognitive-behavioral-journaling",
title: "Try Cognitive Behavioral Journaling",
- imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a",
+ imageUrl: "https://images.unsplash.com/photo-1455390582262-044cdead277a?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Psychology",
estimated_time: "A month",
@@ -1725,7 +1725,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "work-through-platos-republic",
title: "Work Through Plato's Republic",
- imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794",
+ imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Philosophy",
estimated_time: "A week",
@@ -1740,7 +1740,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "join-an-online-philosophy-discussion-group",
title: "Join an Online Philosophy Discussion Group",
- imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f",
+ imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=800&fit=crop&auto=format",
type: "Social",
field: "Philosophy",
estimated_time: "Ongoing",
@@ -1755,7 +1755,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "visit-a-local-historical-museum",
title: "Visit a Local Historical Museum",
- imageUrl: "https://images.unsplash.com/photo-1518998053901-5348d3961a04",
+ imageUrl: "https://images.unsplash.com/photo-1518998053901-5348d3961a04?w=800&fit=crop&auto=format",
type: "Educational",
field: "History",
estimated_time: "An afternoon",
@@ -1770,7 +1770,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "research-your-family-genealogy",
title: "Research Your Family Genealogy",
- imageUrl: "https://images.unsplash.com/photo-1517841905240-472988babdf9",
+ imageUrl: "https://images.unsplash.com/photo-1517841905240-472988babdf9?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "History",
estimated_time: "A week",
@@ -1785,7 +1785,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "start-a-30-day-duolingo-streak",
title: "Start a 30-Day Duolingo Streak",
- imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f",
+ imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=800&fit=crop&auto=format",
type: "Disciplined",
field: "Language",
estimated_time: "A month",
@@ -1800,7 +1800,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "find-a-language-exchange-partner",
title: "Find a Language Exchange Partner",
- imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f",
+ imageUrl: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=800&fit=crop&auto=format",
type: "Social",
field: "Language",
estimated_time: "Ongoing",
@@ -1815,7 +1815,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "write-a-one-page-business-plan",
title: "Write a One-Page Business Plan",
- imageUrl: "https://images.unsplash.com/photo-1520607162513-77705c0f0d4a",
+ imageUrl: "https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?w=800&fit=crop&auto=format",
type: "Professional",
field: "Business",
estimated_time: "A few hours",
@@ -1830,7 +1830,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "attend-a-local-entrepreneur-meetup",
title: "Attend a Local Entrepreneur Meetup",
- imageUrl: "https://images.unsplash.com/photo-1511578314322-379afb476865",
+ imageUrl: "https://images.unsplash.com/photo-1511578314322-379afb476865?w=800&fit=crop&auto=format",
type: "Social",
field: "Business",
estimated_time: "1 hour",
@@ -1845,7 +1845,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-personal-budget-spreadsheet",
title: "Build a Personal Budget Spreadsheet",
- imageUrl: "https://images.unsplash.com/photo-1554224155-6726b3ff858f",
+ imageUrl: "https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=800&fit=crop&auto=format",
type: "Analytical",
field: "Finance",
estimated_time: "A few hours",
@@ -1860,7 +1860,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "read-the-psychology-of-money",
title: "Read The Psychology of Money",
- imageUrl: "https://images.unsplash.com/photo-1520607162513-77705c0f0d4a",
+ imageUrl: "https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Finance",
estimated_time: "A week",
@@ -1875,7 +1875,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "volunteer-to-lead-a-community-project",
title: "Volunteer to Lead a Community Project",
- imageUrl: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac",
+ imageUrl: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=800&fit=crop&auto=format",
type: "Leadership",
field: "Leadership",
estimated_time: "A month",
@@ -1890,7 +1890,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "read-extreme-ownership",
title: "Read Extreme Ownership",
- imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794",
+ imageUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794?w=800&fit=crop&auto=format",
type: "Intellectual",
field: "Leadership",
estimated_time: "A week",
@@ -1905,7 +1905,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "complete-a-10-day-vipassana-course",
title: "Complete a 10-Day Vipassana Course",
- imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773",
+ imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Meditation",
estimated_time: "A week",
@@ -1920,7 +1920,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "set-up-a-daily-5-minute-breath-practice",
title: "Set Up a Daily 5-Minute Breath Practice",
- imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773",
+ imageUrl: "https://images.unsplash.com/photo-1506126613408-eca07ce68773?w=800&fit=crop&auto=format",
type: "Mindful",
field: "Meditation",
estimated_time: "A month",
@@ -1935,7 +1935,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "enroll-in-a-free-online-course",
title: "Enroll in a Free Online Course",
- imageUrl: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3",
+ imageUrl: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&fit=crop&auto=format",
type: "Educational",
field: "Education",
estimated_time: "A month",
@@ -1950,7 +1950,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "teach-a-skill-to-someone-you-know",
title: "Teach a Skill to Someone You Know",
- imageUrl: "https://images.unsplash.com/photo-1513258496099-48168024aec0",
+ imageUrl: "https://images.unsplash.com/photo-1513258496099-48168024aec0?w=800&fit=crop&auto=format",
type: "Social",
field: "Education",
estimated_time: "A few hours",
@@ -1965,7 +1965,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "attend-a-neighborhood-association-meeting",
title: "Attend a Neighborhood Association Meeting",
- imageUrl: "https://images.unsplash.com/photo-1511578314322-379afb476865",
+ imageUrl: "https://images.unsplash.com/photo-1511578314322-379afb476865?w=800&fit=crop&auto=format",
type: "Community-oriented",
field: "Community",
estimated_time: "1 hour",
@@ -1980,7 +1980,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "organize-a-block-party",
title: "Organize a Block Party",
- imageUrl: "https://images.unsplash.com/photo-1492684223066-81342ee5ff30",
+ imageUrl: "https://images.unsplash.com/photo-1492684223066-81342ee5ff30?w=800&fit=crop&auto=format",
type: "Social",
field: "Community",
estimated_time: "A weekend",
@@ -1995,7 +1995,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "sign-up-with-a-local-food-bank",
title: "Sign Up With a Local Food Bank",
- imageUrl: "https://images.unsplash.com/photo-1488521787991-ed7bbaae773c",
+ imageUrl: "https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?w=800&fit=crop&auto=format",
type: "Community-oriented",
field: "Volunteering",
estimated_time: "Ongoing",
@@ -2010,7 +2010,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "mentor-a-high-school-student",
title: "Mentor a High School Student",
- imageUrl: "https://images.unsplash.com/photo-1513258496099-48168024aec0",
+ imageUrl: "https://images.unsplash.com/photo-1513258496099-48168024aec0?w=800&fit=crop&auto=format",
type: "Educational",
field: "Volunteering",
estimated_time: "Ongoing",
@@ -2025,7 +2025,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "build-a-wooden-bookshelf-from-scratch",
title: "Build a Wooden Bookshelf From Scratch",
- imageUrl: "https://images.unsplash.com/photo-1505693416388-ac5ce068fe85",
+ imageUrl: "https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?w=800&fit=crop&auto=format",
type: "Constructive",
field: "DIY",
estimated_time: "A weekend",
@@ -2040,7 +2040,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "rewire-a-vintage-lamp",
title: "Rewire a Vintage Lamp",
- imageUrl: "https://images.unsplash.com/photo-1519710164239-da123dc03ef4",
+ imageUrl: "https://images.unsplash.com/photo-1519710164239-da123dc03ef4?w=800&fit=crop&auto=format",
type: "Constructive",
field: "DIY",
estimated_time: "An afternoon",
@@ -2055,7 +2055,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "redesign-a-room-with-no-new-purchases",
title: "Redesign a Room With No New Purchases",
- imageUrl: "https://images.unsplash.com/photo-1505693416388-ac5ce068fe85",
+ imageUrl: "https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?w=800&fit=crop&auto=format",
type: "Creative",
field: "Home Design",
estimated_time: "An afternoon",
@@ -2070,7 +2070,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "learn-the-basics-of-interior-design",
title: "Learn the Basics of Interior Design",
- imageUrl: "https://images.unsplash.com/photo-1484154218962-a197022b5858",
+ imageUrl: "https://images.unsplash.com/photo-1484154218962-a197022b5858?w=800&fit=crop&auto=format",
type: "Educational",
field: "Home Design",
estimated_time: "A week",
@@ -2085,7 +2085,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "start-a-vinyl-record-collection",
title: "Start a Vinyl Record Collection",
- imageUrl: "https://images.unsplash.com/photo-1460661419201-fd4cecdf8a8b",
+ imageUrl: "https://images.unsplash.com/photo-1460661419201-fd4cecdf8a8b?w=800&fit=crop&auto=format",
type: "Reflective",
field: "Collecting",
estimated_time: "Ongoing",
@@ -2100,7 +2100,7 @@ export const RECOMMENDATIONS: Activity[] = [
{
slug: "spend-a-month-visiting-antique-markets",
title: "Spend a Month Visiting Antique Markets",
- imageUrl: "https://images.unsplash.com/photo-1511578314322-379afb476865",
+ imageUrl: "https://images.unsplash.com/photo-1511578314322-379afb476865?w=800&fit=crop&auto=format",
type: "Exploratory",
field: "Collecting",
estimated_time: "A month",