chore(release): v1.0.0
This commit is contained in:
@@ -36,5 +36,11 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: composer install --no-interaction --prefer-dist
|
run: composer install --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: Set up database
|
||||||
|
run: |
|
||||||
|
APP_ENV=test php bin/console doctrine:database:create --env=test
|
||||||
|
APP_ENV=test php bin/console doctrine:migrations:migrate --no-interaction --env=test
|
||||||
|
APP_ENV=test php bin/console doctrine:fixtures:load --no-interaction --env=test
|
||||||
|
|
||||||
- name: Run PHPUnit
|
- name: Run PHPUnit
|
||||||
run: vendor/bin/phpunit
|
run: vendor/bin/phpunit
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: npx prettier --check .
|
run: npm run check
|
||||||
|
|
||||||
typecheck:
|
typecheck:
|
||||||
name: Type Check
|
name: Type Check
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { ConfirmSignalDialog } from "@/components/app/confirm-signal-dialog";
|
|||||||
import { HeaderOverlay } from "@/components/app/header-overlay";
|
import { HeaderOverlay } from "@/components/app/header-overlay";
|
||||||
import { SidebarPanels } from "@/components/app/sidebar-panels";
|
import { SidebarPanels } from "@/components/app/sidebar-panels";
|
||||||
import { MapViewport } from "@/components/map/map-viewport";
|
import { MapViewport } from "@/components/map/map-viewport";
|
||||||
// Alert dialog moved into a dedicated component
|
|
||||||
import { SidebarProvider, Sidebar, SidebarContent, SidebarInset } from "@/components/ui/sidebar";
|
import { SidebarProvider, Sidebar, SidebarContent, SidebarInset } from "@/components/ui/sidebar";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { useFeedDerivations } from "@/hooks/use-feed-derivations";
|
import { useFeedDerivations } from "@/hooks/use-feed-derivations";
|
||||||
@@ -41,12 +40,7 @@ export default function App() {
|
|||||||
const isRequestingLocation = useAppStore(state => state.isRequestingLocation);
|
const isRequestingLocation = useAppStore(state => state.isRequestingLocation);
|
||||||
const isDetailsOpen = useAppStore(state => state.isSidebarOpen);
|
const isDetailsOpen = useAppStore(state => state.isSidebarOpen);
|
||||||
const setIsDetailsOpen = useAppStore(state => state.setIsSidebarOpen);
|
const setIsDetailsOpen = useAppStore(state => state.setIsSidebarOpen);
|
||||||
const isHeaderCollapsed = useAppStore(state => state.isHeaderCollapsed);
|
|
||||||
const setIsHeaderCollapsed = useAppStore(state => state.setIsHeaderCollapsed);
|
|
||||||
const isMobileHeaderOpen = useAppStore(state => state.isMobileHeaderOpen);
|
|
||||||
const setIsMobileHeaderOpen = useAppStore(state => state.setIsMobileHeaderOpen);
|
|
||||||
const tileProvider = useAppStore(state => state.tileProvider);
|
const tileProvider = useAppStore(state => state.tileProvider);
|
||||||
const setTileProvider = useAppStore(state => state.setTileProvider);
|
|
||||||
const { refresh: refreshLocation } = useUserLocation();
|
const { refresh: refreshLocation } = useUserLocation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -73,8 +67,6 @@ export default function App() {
|
|||||||
[selectVisibleLatestByUser, userLocation]
|
[selectVisibleLatestByUser, userLocation]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Derived lists depend on focusOn; initialize map first, then derive.
|
|
||||||
|
|
||||||
const statusLabel = t(`status.${status}`);
|
const statusLabel = t(`status.${status}`);
|
||||||
const lastUpdatedLabel = lastUpdated ? formatRelativeTime(lastUpdated, locale) : t("common.never");
|
const lastUpdatedLabel = lastUpdated ? formatRelativeTime(lastUpdated, locale) : t("common.never");
|
||||||
const isLoading = status === "loading";
|
const isLoading = status === "loading";
|
||||||
@@ -166,11 +158,9 @@ export default function App() {
|
|||||||
setIsConfirmOpen(true);
|
setIsConfirmOpen(true);
|
||||||
}, [userLocation]);
|
}, [userLocation]);
|
||||||
|
|
||||||
// recentActivity and dangerCells returned above
|
|
||||||
const confirmationLat = pendingSpot ? formatCoordinate(pendingSpot.lat, locale) : "--";
|
const confirmationLat = pendingSpot ? formatCoordinate(pendingSpot.lat, locale) : "--";
|
||||||
const confirmationLng = pendingSpot ? formatCoordinate(pendingSpot.lng, locale) : "--";
|
const confirmationLng = pendingSpot ? formatCoordinate(pendingSpot.lng, locale) : "--";
|
||||||
const isDialogDisabled = !pendingSpot || isConfirming;
|
const isDialogDisabled = !pendingSpot || isConfirming;
|
||||||
const detailsToggleLabel = isDetailsOpen ? t("details.close") : t("details.open");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -187,9 +177,6 @@ export default function App() {
|
|||||||
isBusy={isPosting || isConfirming}
|
isBusy={isPosting || isConfirming}
|
||||||
locationHint={locationHint}
|
locationHint={locationHint}
|
||||||
showLocationCta={showLocationCta}
|
showLocationCta={showLocationCta}
|
||||||
hasLocation={hasLocation}
|
|
||||||
tileProvider={tileProvider}
|
|
||||||
setTileProvider={setTileProvider}
|
|
||||||
dangerCells={dangerCells}
|
dangerCells={dangerCells}
|
||||||
recentActivity={recentActivity}
|
recentActivity={recentActivity}
|
||||||
/>
|
/>
|
||||||
@@ -219,12 +206,6 @@ export default function App() {
|
|||||||
disableHeat={visibleDensity.length === 0}
|
disableHeat={visibleDensity.length === 0}
|
||||||
disableLocate={!hasLocation}
|
disableLocate={!hasLocation}
|
||||||
disableMySignal={!myVisibleSignal}
|
disableMySignal={!myVisibleSignal}
|
||||||
isDetailsOpen={isDetailsOpen}
|
|
||||||
detailsToggleLabel={detailsToggleLabel}
|
|
||||||
isHeaderCollapsed={isHeaderCollapsed}
|
|
||||||
setIsHeaderCollapsed={setIsHeaderCollapsed}
|
|
||||||
isMobileHeaderOpen={isMobileHeaderOpen}
|
|
||||||
setIsMobileHeaderOpen={setIsMobileHeaderOpen}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmSignalDialog
|
<ConfirmSignalDialog
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { AppHeader } from "@/components/layout/app-header";
|
import { AppHeader } from "@/components/layout/app-header";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
|
import { useAppStore } from "@/store/use-app-store";
|
||||||
import type { FeedStatus } from "@/types/api";
|
import type { FeedStatus } from "@/types/api";
|
||||||
|
|
||||||
interface HeaderOverlayProps {
|
interface HeaderOverlayProps {
|
||||||
@@ -18,12 +19,6 @@ interface HeaderOverlayProps {
|
|||||||
disableHeat: boolean;
|
disableHeat: boolean;
|
||||||
disableLocate: boolean;
|
disableLocate: boolean;
|
||||||
disableMySignal: boolean;
|
disableMySignal: boolean;
|
||||||
isDetailsOpen: boolean;
|
|
||||||
detailsToggleLabel: string;
|
|
||||||
isHeaderCollapsed: boolean;
|
|
||||||
setIsHeaderCollapsed: (collapsed: boolean) => void;
|
|
||||||
isMobileHeaderOpen: boolean;
|
|
||||||
setIsMobileHeaderOpen: (open: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeaderOverlay({
|
export function HeaderOverlay({
|
||||||
@@ -38,14 +33,14 @@ export function HeaderOverlay({
|
|||||||
disableHeat,
|
disableHeat,
|
||||||
disableLocate,
|
disableLocate,
|
||||||
disableMySignal,
|
disableMySignal,
|
||||||
isDetailsOpen,
|
|
||||||
detailsToggleLabel,
|
|
||||||
isHeaderCollapsed,
|
|
||||||
setIsHeaderCollapsed,
|
|
||||||
isMobileHeaderOpen,
|
|
||||||
setIsMobileHeaderOpen,
|
|
||||||
}: HeaderOverlayProps) {
|
}: HeaderOverlayProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const isDetailsOpen = useAppStore(state => state.isSidebarOpen);
|
||||||
|
const isHeaderCollapsed = useAppStore(state => state.isHeaderCollapsed);
|
||||||
|
const setIsHeaderCollapsed = useAppStore(state => state.setIsHeaderCollapsed);
|
||||||
|
const isMobileHeaderOpen = useAppStore(state => state.isMobileHeaderOpen);
|
||||||
|
const setIsMobileHeaderOpen = useAppStore(state => state.setIsMobileHeaderOpen);
|
||||||
|
const detailsToggleLabel = isDetailsOpen ? t("details.close") : t("details.open");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none absolute inset-0 flex flex-col">
|
<div className="pointer-events-none absolute inset-0 flex flex-col">
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { MapTilesPanel } from "@/components/panels/map-tiles-panel";
|
|||||||
import { OverviewPanel } from "@/components/panels/overview-panel";
|
import { OverviewPanel } from "@/components/panels/overview-panel";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import type { FeedError } from "@/hooks/use-hotsop-feed";
|
import type { FeedError } from "@/hooks/use-hotsop-feed";
|
||||||
import type { TileProvider } from "@/hooks/use-leaflet-heatmap";
|
|
||||||
import { formatRelativeTime } from "@/lib/utils";
|
import { formatRelativeTime } from "@/lib/utils";
|
||||||
|
import { useAppStore } from "@/store/use-app-store";
|
||||||
|
|
||||||
type DangerCell = {
|
type DangerCell = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -36,9 +36,6 @@ interface SidebarPanelsProps {
|
|||||||
isBusy: boolean;
|
isBusy: boolean;
|
||||||
locationHint: string;
|
locationHint: string;
|
||||||
showLocationCta: boolean;
|
showLocationCta: boolean;
|
||||||
hasLocation: boolean;
|
|
||||||
tileProvider: TileProvider;
|
|
||||||
setTileProvider: (p: TileProvider) => void;
|
|
||||||
dangerCells: DangerCell[];
|
dangerCells: DangerCell[];
|
||||||
recentActivity: ActivityItem[];
|
recentActivity: ActivityItem[];
|
||||||
}
|
}
|
||||||
@@ -53,14 +50,12 @@ export function SidebarPanels({
|
|||||||
isBusy,
|
isBusy,
|
||||||
locationHint,
|
locationHint,
|
||||||
showLocationCta,
|
showLocationCta,
|
||||||
hasLocation,
|
|
||||||
tileProvider,
|
|
||||||
setTileProvider,
|
|
||||||
dangerCells,
|
dangerCells,
|
||||||
recentActivity,
|
recentActivity,
|
||||||
}: SidebarPanelsProps) {
|
}: SidebarPanelsProps) {
|
||||||
const { i18n, t } = useTranslation();
|
const { i18n, t } = useTranslation();
|
||||||
const locale = i18n.language === "fr" ? "fr-FR" : "en-US";
|
const locale = i18n.language === "fr" ? "fr-FR" : "en-US";
|
||||||
|
const hasLocation = useAppStore(state => Boolean(state.userLocation));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
@@ -80,7 +75,7 @@ export function SidebarPanels({
|
|||||||
/>
|
/>
|
||||||
<HotspotStatsPanel hasLocation={hasLocation} radiusKm={1} locationHint={locationHint} cells={dangerCells} />
|
<HotspotStatsPanel hasLocation={hasLocation} radiusKm={1} locationHint={locationHint} cells={dangerCells} />
|
||||||
<ActivityPanel items={recentActivity} emptyMessage={t("activity.empty")} />
|
<ActivityPanel items={recentActivity} emptyMessage={t("activity.empty")} />
|
||||||
<MapTilesPanel tileProvider={tileProvider} setTileProvider={setTileProvider} />
|
<MapTilesPanel />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import type { TileProvider } from "@/hooks/use-leaflet-heatmap";
|
import type { TileProvider } from "@/hooks/use-leaflet-heatmap";
|
||||||
|
import { useAppStore } from "@/store/use-app-store";
|
||||||
|
|
||||||
interface MapTilesPanelProps {
|
export function MapTilesPanel() {
|
||||||
tileProvider: TileProvider;
|
|
||||||
setTileProvider: (provider: TileProvider) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MapTilesPanel({ tileProvider, setTileProvider }: MapTilesPanelProps) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const tileProvider = useAppStore(state => state.tileProvider);
|
||||||
|
const setTileProvider = useAppStore(state => state.setTileProvider);
|
||||||
const tileOptions = useMemo(
|
const tileOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: "openstreetmap" as TileProvider, label: t("map.tiles.openstreetmap") },
|
{ value: "openstreetmap" as TileProvider, label: t("map.tiles.openstreetmap") },
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
KERNEL_CLASS='App\Kernel'
|
KERNEL_CLASS='App\Kernel'
|
||||||
APP_SECRET='$ecretf0rt3st'
|
APP_SECRET='$ecretf0rt3st'
|
||||||
CORS_ALLOW_ORIGIN='^https?://(?:localhost|127\\.0\\.0\\.1)(?::[0-9]+)?$'
|
CORS_ALLOW_ORIGIN='^https?://(?:localhost|127\\.0\\.0\\.1)(?::[0-9]+)?$'
|
||||||
|
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ class SignalController
|
|||||||
'clientKey' => $clientKey,
|
'clientKey' => $clientKey,
|
||||||
'point' => [
|
'point' => [
|
||||||
'id' => $signal->id->toString(),
|
'id' => $signal->id->toString(),
|
||||||
'signalLocation' => $signal->signalLocation,
|
'signalLocation' => [
|
||||||
|
'lat' => $signal->signalLocation->getLat(),
|
||||||
|
'lng' => $signal->signalLocation->getLng(),
|
||||||
|
],
|
||||||
'createdAt' => $signal->createdAt->format(DATE_ATOM),
|
'createdAt' => $signal->createdAt->format(DATE_ATOM),
|
||||||
'userKey' => $signal->userKey,
|
'userKey' => $signal->userKey,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
use App\Entity\Signal;
|
use App\Entity\Signal;
|
||||||
use App\ValueObject\Point;
|
|
||||||
|
|
||||||
class SignalSnapshotBuilder
|
class SignalSnapshotBuilder
|
||||||
{
|
{
|
||||||
@@ -15,14 +14,14 @@ class SignalSnapshotBuilder
|
|||||||
* @return array{
|
* @return array{
|
||||||
* points: list<array{
|
* points: list<array{
|
||||||
* id: string,
|
* id: string,
|
||||||
* signalLocation: Point,
|
* signalLocation: array{lat: float, lng: float},
|
||||||
* createdAt: string,
|
* createdAt: string,
|
||||||
* userKey: string,
|
* userKey: string,
|
||||||
* }>,
|
* }>,
|
||||||
* density: list<array{lat: float, lng: float, intensity: int}>,
|
* density: list<array{lat: float, lng: float, intensity: int}>,
|
||||||
* latestByUser: list<array{
|
* latestByUser: list<array{
|
||||||
* id: string,
|
* id: string,
|
||||||
* signalLocation: Point,
|
* signalLocation: array{lat: float, lng: float},
|
||||||
* createdAt: string,
|
* createdAt: string,
|
||||||
* userKey: string,
|
* userKey: string,
|
||||||
* }>,
|
* }>,
|
||||||
@@ -38,7 +37,10 @@ class SignalSnapshotBuilder
|
|||||||
foreach ($signals as $signal) {
|
foreach ($signals as $signal) {
|
||||||
$point = [
|
$point = [
|
||||||
'id' => $signal->id->toString(),
|
'id' => $signal->id->toString(),
|
||||||
'signalLocation' => $signal->signalLocation,
|
'signalLocation' => [
|
||||||
|
'lat' => $signal->signalLocation->getLat(),
|
||||||
|
'lng' => $signal->signalLocation->getLng(),
|
||||||
|
],
|
||||||
'createdAt' => $signal->createdAt->format(DATE_ATOM),
|
'createdAt' => $signal->createdAt->format(DATE_ATOM),
|
||||||
'userKey' => $signal->userKey,
|
'userKey' => $signal->userKey,
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user