Skip to content

Commit

Permalink
feat!: add water statuses
Browse files Browse the repository at this point in the history
  • Loading branch information
macojaune authored Oct 24, 2024
2 parents b1fabb5 + 9d76790 commit dbad91d
Show file tree
Hide file tree
Showing 10 changed files with 477 additions and 153 deletions.
21 changes: 16 additions & 5 deletions server/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { sqliteTable, real, text, integer } from "drizzle-orm/sqlite-core"
import { sqliteTable, real, text, integer } from "drizzle-orm/sqlite-core";

export const powerStatuses = sqliteTable("power_statuses", {
id: text("id").primaryKey(),
latitude: real("latitude").notNull(),
longitude: real("longitude").notNull(),
hasPower: integer("has_power", { mode: "boolean" }).notNull(),
isOn: integer("has_power", { mode: "boolean" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
})
});

export type PowerStatus = typeof powerStatuses.$inferSelect
export type InsertPowerStatus = typeof powerStatuses.$inferInsert
export type PowerStatus = typeof powerStatuses.$inferSelect;
export type InsertPowerStatus = typeof powerStatuses.$inferInsert;

export const waterStatuses = sqliteTable("water_statuses", {
id: text("id").primaryKey(),
latitude: real("latitude").notNull(),
longitude: real("longitude").notNull(),
isOn: integer("has_water", { mode: "boolean" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});

export type WaterStatus = typeof waterStatuses.$inferSelect;
export type InsertWaterStatus = typeof waterStatuses.$inferInsert;
78 changes: 70 additions & 8 deletions server/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import { Hono } from "hono";
import { powerStatuses } from "./db/schema";
import { powerStatuses, waterStatuses } from "./db/schema";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { db } from "./db";
import { desc, getTableColumns, gte, sql } from "drizzle-orm";

const obfuscateCoordinate = (coord: number, factor = 0.001) => {
return coord + (Math.random() - 0.667) * factor;
};

const app = new Hono();
app.use(logger());

app.use(
"/api/*",
cors({
origin: [
"http://localhost:5173",
"https://pani-limie.netlify.app",
"https://www.pani-limie.netlify.app",
"https://pani-limie.marvinl.com",
"https://pani-limye.marvinl.com",
"https://www.pani-limye.marvinl.com",
"https://pani-limie.marvinl.com",
"https://www.pani-limie.marvinl.com",
"https://pani-dlo.marvinl.com",
"https://www.pani-dlo.marvinl.com",
],
}),
);
Expand All @@ -27,15 +33,24 @@ app.get("/", (c) => c.text("Pani limyè!"));

app.get("/api/power-statuses", async (c) => {
try {
const statuses = await db.query.powerStatuses.findMany({
where: (ps, { gte }) =>
gte(ps.createdAt, new Date(Date.now() - 6 * 60 * 60 * 1000)), //limit to 6 hours ago
});
const statuses = await db
.select({
...getTableColumns(powerStatuses),
lastStatus: sql`MAX(${powerStatuses.createdAt})`,
})
.from(powerStatuses)
.where(
gte(powerStatuses.createdAt, new Date(Date.now() - 6 * 60 * 60 * 1000)),
) //limit to 6 hours ago
.orderBy(desc(powerStatuses.createdAt))
.groupBy(powerStatuses.latitude, powerStatuses.longitude);

const obscuredStatuses = statuses.map((status) => ({
...status,
latitude: obfuscateCoordinate(status.latitude),
longitude: obfuscateCoordinate(status.longitude),
}));

return c.json(obscuredStatuses);
} catch (error) {
console.error("Error fetching power statuses:", error);
Expand All @@ -45,14 +60,14 @@ app.get("/api/power-statuses", async (c) => {

app.post("/api/power-status", async (c) => {
try {
const { latitude, longitude, hasPower } = await c.req.json();
const { latitude, longitude, isOn } = await c.req.json();
const newStatus = await db
.insert(powerStatuses)
.values({
id: crypto.randomUUID(),
latitude,
longitude,
hasPower,
isOn,
createdAt: new Date(),
})
.returning()
Expand All @@ -64,6 +79,53 @@ app.post("/api/power-status", async (c) => {
}
});

app.get("/api/water-statuses", async (c) => {
try {
const statuses = await db
.select({
...getTableColumns(waterStatuses),
lastStatus: sql`MAX(${waterStatuses.createdAt})`,
})
.from(waterStatuses)
.where(
gte(waterStatuses.createdAt, new Date(Date.now() - 6 * 60 * 60 * 1000)),
) //limit to 6 hours ago
.orderBy(desc(waterStatuses.createdAt))
.groupBy(waterStatuses.latitude, waterStatuses.longitude);

const obscuredStatuses = statuses.map((status) => ({
...status,
latitude: obfuscateCoordinate(status.latitude),
longitude: obfuscateCoordinate(status.longitude),
}));
return c.json(obscuredStatuses);
} catch (error) {
console.error("Error fetching water statuses:", error);
return c.json({ error: "Failed to fetch water statuses" }, 500);
}
});

app.post("/api/water-status", async (c) => {
try {
const { latitude, longitude, isOn } = await c.req.json();
const newStatus = await db
.insert(waterStatuses)
.values({
id: crypto.randomUUID(),
latitude,
longitude,
isOn,
createdAt: new Date(),
})
.returning()
.get();
return c.json(newStatus, 201);
} catch (error) {
console.error("Error creating water status:", error);
return c.json({ error: "Failed to create water status" }, 500);
}
});

const port = 3000;
console.log(`Server is running on port ${port}`);

Expand Down
52 changes: 52 additions & 0 deletions src/components/status-history-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { FC } from "react";
import { type Map } from "leaflet";
import { twMerge } from "tailwind-merge";
import TimeAgo from "./time-ago";

const StatusHistoryItem: FC<{
latitude: number;
longitude: number;
isOn: boolean;
createdAt: Date;
map: Map | null;
type: string | "power" | "water";
}> = ({ latitude, longitude, isOn, createdAt, map, type }) => {
return (
<button
onClick={() =>
map?.flyTo([latitude, longitude], 16, {
animate: true,
duration: 0.8,
})
}
className={twMerge(
"flex w-full items-center justify-between rounded border border-amber-500/30 bg-amber-400/20 p-2 font-mono text-xs",
type === "water" && "border-cyan-500/30 bg-cyan-200 text-slate-800",
!isOn && "border-slate-800 bg-slate-800 text-yellow-400",
!isOn && type === "water" && "text-white bg-cyan-950",
)}
>
<div
className={twMerge(
"flex w-full flex-row items-center justify-between gap-2 font-sans",
)}
>
{type === "power" &&
(isOn ? (
<span className="mr-2 rounded-full bg-white p-1 text-3xl">💡</span>
) : (
<span className="mr-2 rounded-full bg-white p-1 text-3xl">🕯️</span>
))}
{type === "water" &&
(isOn ? (
<span className="mr-2 rounded-full bg-white p-1 text-3xl">🚰</span>
) : (
<span className="mr-2 rounded-full bg-white p-1 text-3xl">🚱</span>
))}
clique pour le voir sur la carte
<TimeAgo date={createdAt} className="ml-auto" />
</div>
</button>
);
};
export default StatusHistoryItem;
81 changes: 51 additions & 30 deletions src/components/status-marker-list.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import { Lightbulb, LightbulbOff } from "lucide-react";
import { Droplets, Lightbulb, LightbulbOff, Milk } from "lucide-react";
import { useState } from "react";
import { Marker, Popup, useMapEvents } from "react-leaflet";
import MarkerClusterGroup from "react-leaflet-cluster";
import { type PowerStatus } from "../../server/db/schema";
import { WaterStatus, type PowerStatus } from "../../server/db/schema";
import TimeAgo from "./time-ago";
import { divIcon, point } from "leaflet";
import { counting } from "radash";
import { twMerge } from "tailwind-merge";

const StatusList = ({
powerStatuses,
}: {
powerStatuses: PowerStatus[] | undefined;
}) => {
statuses,
type,
}:
| {
statuses: PowerStatus[] | undefined;
type: "power";
}
| {
statuses: WaterStatus[] | undefined;
type: "water";
}) => {
const [userLocation, setUserLocation] = useState<[number, number] | null>(
null,
);
const [test, setTest] = useState(null);
const map = useMapEvents({
locationfound: (e) => {
console.log("location found", e);
setUserLocation([e.latlng.lat, e.latlng.lng]);
map.fitBounds(
powerStatuses?.map(({ latitude, longitude }) => [
latitude,
longitude,
]) as [number, number][],
statuses?.map(({ latitude, longitude }) => [latitude, longitude]) as [
number,
number,
][],
{
animate: true,
duration: 0.2,
Expand All @@ -40,20 +45,27 @@ const StatusList = ({
{userLocation && <Marker position={userLocation} />}
<MarkerClusterGroup
iconCreateFunction={(cluster) => {
const markers = cluster.getAllChildMarkers();
const res = counting(markers, (m) =>
m.options.title === "💡" ? "light" : "dark",
);
const markers = cluster.getAllChildMarkers().sort((a, b) => {
const first = a.options.title!.split("_");
const second = b.options.title!.split("_");
return new Date(second[2]).getTime() - new Date(first[2]).getTime();
});
const [type, latestStatus, _] = markers[0].options?.title!.split("_");

return divIcon({
html: `<div class="aspect-square flex items-center justify-center">
<span class="text-base font-medium text-white">${cluster.getChildCount()}</div>
<span class="text-base font-medium text-white">${cluster.getChildCount()}</span>
</div>`,
className: twMerge(
"border-4 rounded-full ",
(res?.light ?? 0) > (res?.dark ?? 0)
? "border-green-500/10 bg-green-500/90"
: "border-red-500/10 bg-red-500/90",
type === "power" &&
(latestStatus === "on"
? "border-green-500/10 bg-green-500/90"
: "border-red-500/10 bg-red-500/90"),
type === "water" &&
(latestStatus === "on"
? "border-cyan-500/10 bg-cyan-500/90"
: "border-cyan-900/10 bg-cyan-900/90"),
),
iconSize: point(60, 60),
});
Expand All @@ -66,25 +78,34 @@ const StatusList = ({
showCoverageOnHover
zoomToBoundsOnClick
>
{powerStatuses?.map(
({ id, latitude, longitude, hasPower, createdAt }) => (
{statuses?.map((data) => {
const { id, latitude, longitude, createdAt, isOn } = data;

return (
<Marker
key={id}
position={[latitude, longitude]}
title={hasPower ? "💡" : "🕯️"}
title={`${type}_${isOn ? "on" : "off"}_${createdAt}`}
>
<Popup>
{hasPower ? (
<Lightbulb size={44} className="text-green-500" />
) : (
<LightbulbOff size={44} className="text-red-500" />
)}
{type === "power" &&
(isOn ? (
<Lightbulb size={44} className="text-green-500" />
) : (
<LightbulbOff size={44} className="text-red-500" />
))}
{type === "water" &&
(isOn ? (
<Droplets size={44} className="text-cyan-500" />
) : (
<Milk size={44} className="text-cyan-950" />
))}
<br />
<TimeAgo date={createdAt} />
</Popup>
</Marker>
),
)}
);
})}
</MarkerClusterGroup>
</>
);
Expand Down
Loading

0 comments on commit dbad91d

Please sign in to comment.