// DEPLOY_CONFIG: {"triggers": [{"name": "notify-members-on-harvest-logged", "on": "collection.add", "collection": "harvests", "actions": [{"type": "notification", "to": "all_members", "subject": "New harvest logged", "body": "A new harvest has been logged. Check the app for details."}]}], "cron": [{"name": "weekly-garden-status-email-digest", "schedule": "0 9 * * 1", "action": "event", "config": {"event_type": "weekly-garden-status-email-digest"}}]}
import { useEffect, useMemo, useState } from 'react';
import { useAuth, usePresence, useProxy } from '@deplixo/sdk';
import { GardenGrid } from './components/GardenGrid.jsx';
import { HarvestCalendar } from './components/HarvestCalendar.jsx';
import { MyPlots } from './components/MyPlots.jsx';
import { PlotModal } from './components/PlotModal.jsx';
function PresenceBar({ currentUser }) {
const { users, update } = usePresence({
status: 'online',
name: currentUser?.name,
avatar: currentUser?.avatar,
email: currentUser?.email,
});
const [presenceState, setPresenceState] = useState('online');
const [lastActivityAt, setLastActivityAt] = useState(Date.now());
useEffect(() => {
const setActive = () => {
setPresenceState('online');
setLastActivityAt(Date.now());
update?.({
status: 'online',
lastSeenAt: Date.now(),
});
};
const setIdle = () => {
setPresenceState('idle');
update?.({
status: 'idle',
lastSeenAt: Date.now(),
});
};
const handleVisibilityChange = () => {
if (document.hidden) {
setIdle();
} else {
setActive();
}
};
const activityEvents = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll'];
const idleTimer = setInterval(() => {
if (Date.now() - lastActivityAt > 60 * 1000) {
setIdle();
}
}, 15000);
activityEvents.forEach(eventName => window.addEventListener(eventName, setActive, { passive: true }));
document.addEventListener('visibilitychange', handleVisibilityChange);
setActive();
return () => {
activityEvents.forEach(eventName => window.removeEventListener(eventName, setActive));
document.removeEventListener('visibilitychange', handleVisibilityChange);
clearInterval(idleTimer);
update?.({ status: 'offline', lastSeenAt: Date.now() });
};
}, [lastActivityAt, update]);
const presentUsers = useMemo(() => {
return (users || []).filter(user => user?.status !== 'offline');
}, [users]);
return (
{presentUsers.length} {presentUsers.length === 1 ? 'member' : 'members'} at the garden
{presentUsers.length === 0 ? (
No one is active right now
) : (
presentUsers.map(user => (
{user.avatar ? (
) : (
{user.name?.charAt(0)?.toUpperCase() || 'G'}
)}
{user.name || 'Member'}
{user.status || 'online'}
))
)}
);
}
function App() {
const { user, loading, login, logout } = useAuth();
const [activeTab, setActiveTab] = useState('garden');
const [selectedPlot, setSelectedPlot] = useState(null);
const tabs = [
{ id: 'garden', label: '๐ฑ Garden Map', icon: '๐บ๏ธ' },
{ id: 'myplots', label: '๐ฅ My Plots', icon: '๐ฅ' },
{ id: 'calendar', label: '๐
Harvests', icon: '๐
' },
];
if (loading) {
return Signing in...
;
}
if (!user) {
return (
๐ฟ Community Garden
Sign in with Google to join your members garden.
);
}
return (
{activeTab === 'garden' && }
{activeTab === 'myplots' && }
{activeTab === 'calendar' && }
{selectedPlot !== null && (
setSelectedPlot(null)} />
)}
);
}
function WeatherAdviceBar() {
const { fetch: proxyFetch, loading: proxyLoading, error: proxyError } = useProxy();
const [weather, setWeather] = useState(null);
const [advice, setAdvice] = useState([]);
const [locationLabel, setLocationLabel] = useState('your area');
useEffect(() => {
let cancelled = false;
const loadWeather = async () => {
try {
const forecast = await proxyFetch('https://api.weatherapi.com/v1/forecast.json?key=${WEATHER_KEY}&q=auto:ip&days=3&aqi=no&alerts=no', {
method: 'GET'
});
if (cancelled || !forecast) return;
setWeather(forecast);
setLocationLabel(forecast?.location?.name ? `${forecast.location.name}, ${forecast.location.region || forecast.location.country || ''}`.trim().replace(/,$/, '') : 'your area');
} catch (err) {
if (!cancelled) {
setWeather(null);
}
}
};
loadWeather();
return () => {
cancelled = true;
};
}, [proxyFetch]);
useEffect(() => {
const nextAdvice = [];
const currentTempC = weather?.current?.temp_c;
const chanceOfRain = weather?.forecast?.forecastday?.[0]?.day?.daily_chance_of_rain;
const willFreezeNight = weather?.forecast?.forecastday?.some(day => (day?.day?.mintemp_c ?? 99) <= 1) || false;
const conditionText = weather?.current?.condition?.text || '';
if (typeof currentTempC === 'number') {
if (currentTempC < 5) {
nextAdvice.push('Too cold for sensitive seedlings: start indoors or wait for warmer soil.');
} else if (currentTempC <= 12) {
nextAdvice.push('Good for cool-season crops like lettuce, peas, spinach, and radishes.');
} else if (currentTempC <= 24) {
nextAdvice.push('Great planting weather for most vegetables and herbs.');
} else {
nextAdvice.push('Warm conditions: water new transplants well and mulch to conserve moisture.');
}
}
if (typeof chanceOfRain === 'number') {
if (chanceOfRain >= 70) {
nextAdvice.push('Rain is likely soon: avoid heavy watering and consider direct sowing hardy seeds.');
} else if (chanceOfRain >= 35) {
nextAdvice.push('Moderate rain chance: water lightly and watch for damp-soil fungal issues.');
} else {
nextAdvice.push('Low rain chance: plan to water after planting if the topsoil dries out.');
}
}
if (willFreezeNight) {
nextAdvice.push('Frost risk ahead: protect tender plants with covers or delay planting warm-season crops.');
}
if (/storm|thunder|snow|sleet|freezing/i.test(conditionText)) {
nextAdvice.push('Adverse conditions detected: avoid transplanting today if possible.');
}
if (nextAdvice.length === 0) {
nextAdvice.push('Weather data is loading. Check back for planting guidance in a moment.');
}
setAdvice(nextAdvice);
}, [weather]);
return (
๐ฆ๏ธ Planting forecast
{locationLabel}
{proxyLoading && Loading weather...}
{!proxyLoading && weather?.current && (
{weather.current.temp_c}ยฐC ยท {weather.current.condition?.text || 'Current conditions'}
)}
{proxyError && !weather ? (
Weather preview unavailable right now. Please try again later.
) : (
<>
{advice.map((item, index) => (
-
{item}
))}
{weather?.forecast?.forecastday?.[0] && (
{weather.forecast.forecastday.slice(0, 3).map(day => (
{day.date}
{day.day?.avgtemp_c}ยฐC avg
{day.day?.daily_chance_of_rain || 0}% rain
))}
)}
>
)}
);
}
ReactDOM.createRoot(document.getElementById("root")).render();