Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: add water statuses #3

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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