diff --git a/www/backend/helpar/brands/schema.py b/www/backend/helpar/brands/schema.py
index e8a375a..d111b8b 100644
--- a/www/backend/helpar/brands/schema.py
+++ b/www/backend/helpar/brands/schema.py
@@ -33,3 +33,9 @@ class CreateMessageRequest(BaseModel):
class CanonicalBrandInfoRequest(BaseModel):
canonical_name: str
+
+
+class BrandDashboardRequest(BaseModel):
+ start_date: str
+ end_date: str
+ brand_id: UUID4
diff --git a/www/backend/helpar/brands/urls.py b/www/backend/helpar/brands/urls.py
index be622ec..8627ce3 100644
--- a/www/backend/helpar/brands/urls.py
+++ b/www/backend/helpar/brands/urls.py
@@ -33,4 +33,5 @@
path("brands//threads//messages/", brand_messages, name="brand-messages"),
path(r"brands//products//faqs/", brand_product_faqs),
path(r"brands/canonical-brand-info//", canonical_brand_info_view),
+ path(r"brands//dashboard/", brand_dashboard_view),
]
diff --git a/www/backend/helpar/brands/uses.py b/www/backend/helpar/brands/uses.py
index f1749eb..1eceb7a 100644
--- a/www/backend/helpar/brands/uses.py
+++ b/www/backend/helpar/brands/uses.py
@@ -44,3 +44,7 @@ def post_llm_prompt(message: Message) -> Optional[Message]:
return Message.objects.create(
**{"content": response, "thread": message.thread, "actor": LLM_ACTOR, "created_at": timezone.now()}
)
+
+
+def brand_dashboard(params: BrandDashboardRequest) -> Tuple[status, Dict[str, Any]]:
+ return (status.HTTP_200_OK, {})
diff --git a/www/backend/helpar/brands/views.py b/www/backend/helpar/brands/views.py
index d603e67..7e82328 100644
--- a/www/backend/helpar/brands/views.py
+++ b/www/backend/helpar/brands/views.py
@@ -1,3 +1,4 @@
+import logging
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view
@@ -13,6 +14,9 @@
from helpar.lib.constants import *
+logger = logging.getLogger("helpar")
+
+
class BrandViewSet(BaseViewSet):
queryset = Brand.objects.all()
model = Brand
@@ -335,3 +339,16 @@ def create(self, request: Request, brand_id: str, thread_id: str, *args, **kwarg
messages = [message, llm_message]
serializer = self.serializer_class(messages, many=True)
return Response(status=status.HTTP_201_CREATED, data={"data": serializer.data})
+
+
+@api_view(["GET"])
+def brand_dashboard_view(request: Request, brand_id: uuid.UUID) -> Response:
+ try:
+ start_date = request.query_params["start_date"]
+ end_date = request.query_params["end_date"]
+ params = BrandDashboardRequest(start_date=start_date, end_date=end_date, brand_id=brand_id)
+ (resp_status, data) = brand_dashboard(params)
+ return Response(status=resp_status, data={"success": True, "data": data})
+ except Exception as err:
+ logger.error("Error retrieving dashboard data: %s", str(err))
+ return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, data={"success": False, "details": str(err)})
diff --git a/www/backend/helpar/helpar/lib/middlewares.py b/www/backend/helpar/helpar/lib/middlewares.py
index f52be2d..ab256a6 100644
--- a/www/backend/helpar/helpar/lib/middlewares.py
+++ b/www/backend/helpar/helpar/lib/middlewares.py
@@ -27,7 +27,7 @@ def __call__(self, request: Request) -> MiddlewareResponse:
class CatchallMiddleware(BaseMiddleware):
def __init__(self, get_response: Callable[[Request], MiddlewareResponse]):
super().__init__(get_response)
- self.logger = logging.getLogger(__name__)
+ self.logger = logging.getLogger("helpar")
@staticmethod
def _normalize_error_msg(msg: str) -> str:
@@ -60,7 +60,7 @@ def __call__(self, request: Request):
class LoggingMiddleware(BaseMiddleware):
def __init__(self, get_response: Callable[[Request], MiddlewareResponse]):
super().__init__(get_response)
- self.logger = logging.getLogger(__name__)
+ self.logger = logging.getLogger("helpar")
def __call__(self, request: Request) -> MiddlewareResponse:
method = request.method or ""
@@ -99,7 +99,7 @@ def is_unauthenticated_action(self, request: Request) -> bool:
class JwtAuthMiddleware(BaseMiddleware):
def __init__(self, get_response: Callable[[Request], MiddlewareResponse]):
super().__init__(get_response)
- self.logger = logging.getLogger(__name__)
+ self.logger = logging.getLogger("helpar")
self.unauthenticated_routes = UnauthenticatedRoutes()
def __call__(self, request: Request) -> MiddlewareResponse:
diff --git a/www/backend/helpar/helpar/lib/services.py b/www/backend/helpar/helpar/lib/services.py
index c4eafda..1cb1180 100644
--- a/www/backend/helpar/helpar/lib/services.py
+++ b/www/backend/helpar/helpar/lib/services.py
@@ -1,6 +1,7 @@
import logging
import base64
import json
+import uuid
import datetime as dt
import jwt
import httpx
@@ -8,7 +9,7 @@
from django.conf import settings
from t import *
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("helpar")
client = OpenAI(
api_key=settings.OPENAI_API_KEY,
@@ -138,3 +139,42 @@ def _decode_jwt(token: str):
decoded_bytes = base64.urlsafe_b64decode(payload)
decoded_payload = decoded_bytes.decode("utf-8")
return json.loads(decoded_payload)
+
+
+# TODO: We can cache these somehow so that a given brand and set of dates
+# only needs to be queried once
+
+
+class DashboardStatistics:
+ def __init__(self, brand_id: uuid.UUID, start_date: str, end_date: str):
+ self.brand_id = brand_id
+ self.start_date = start_date
+ self.end_date = end_date
+ self._data = {
+ "num_customers": 0,
+ "preferences_recorded": 0,
+ "frustrations_resolved": 0,
+ "reorder_revenue": 0,
+ "customer_data": {
+ "nps": 0,
+ "customer_types": {},
+ "registrations": [
+ {
+ "user_name": "",
+ "user_identifier": "",
+ }
+ ],
+ },
+ "setup_process": {
+ "num_happy_setups": 0,
+ "num_sad_setups": 0,
+ "num_neutral_setups": 0,
+ "setup_process_breakdown": {},
+ },
+ "reorder_revenue_breakdown": {},
+ "support_topics": {},
+ "social_clicks": {},
+ }
+
+ def data(self) -> Dict[str, Any]:
+ return self._data
diff --git a/www/backend/helpar/helpar/settings/__init__.py b/www/backend/helpar/helpar/settings/__init__.py
index de33cfc..41049a7 100644
--- a/www/backend/helpar/helpar/settings/__init__.py
+++ b/www/backend/helpar/helpar/settings/__init__.py
@@ -8,7 +8,7 @@
datefmt="%Y/%m/%d %H:%M:%S",
)
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("helpar")
env = os.environ["ENV"]
proc = multiprocessing.current_process()
diff --git a/www/backend/helpar/users/migrations/0018_oauthrequest_enterprise.py b/www/backend/helpar/users/migrations/0018_oauthrequest_enterprise.py
new file mode 100644
index 0000000..af94b63
--- /dev/null
+++ b/www/backend/helpar/users/migrations/0018_oauthrequest_enterprise.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.0 on 2023-12-30 16:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("users", "0017_alter_user_username"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="oauthrequest",
+ name="enterprise",
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/www/backend/helpar/users/migrations/0019_pkcerequest_enterprise.py b/www/backend/helpar/users/migrations/0019_pkcerequest_enterprise.py
new file mode 100644
index 0000000..cf18859
--- /dev/null
+++ b/www/backend/helpar/users/migrations/0019_pkcerequest_enterprise.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.0 on 2023-12-30 16:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("users", "0018_oauthrequest_enterprise"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="pkcerequest",
+ name="enterprise",
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/www/backend/helpar/users/migrations/0020_session.py b/www/backend/helpar/users/migrations/0020_session.py
new file mode 100644
index 0000000..d26bdf9
--- /dev/null
+++ b/www/backend/helpar/users/migrations/0020_session.py
@@ -0,0 +1,37 @@
+# Generated by Django 5.0 on 2023-12-31 03:11
+
+import django.db.models.deletion
+import uuid
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("users", "0019_pkcerequest_enterprise"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Session",
+ fields=[
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("enterprise", models.BooleanField(default=False)),
+ ("created_at", models.DateTimeField()),
+ (
+ "user",
+ models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="users.user"),
+ ),
+ ],
+ options={
+ "db_table": "sessions",
+ },
+ ),
+ ]
diff --git a/www/backend/helpar/users/migrations/0021_delete_session.py b/www/backend/helpar/users/migrations/0021_delete_session.py
new file mode 100644
index 0000000..5223a5f
--- /dev/null
+++ b/www/backend/helpar/users/migrations/0021_delete_session.py
@@ -0,0 +1,15 @@
+# Generated by Django 5.0 on 2023-12-31 03:14
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("users", "0020_session"),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name="Session",
+ ),
+ ]
diff --git a/www/backend/helpar/users/models.py b/www/backend/helpar/users/models.py
index 887178b..4b2becc 100644
--- a/www/backend/helpar/users/models.py
+++ b/www/backend/helpar/users/models.py
@@ -46,6 +46,7 @@ class Meta:
code: models.CharField = models.CharField(max_length=255, unique=True, db_index=True)
user: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE)
+ enterprise: models.BooleanField = models.BooleanField(default=False)
created_at: models.DateTimeField = models.DateTimeField(null=True)
expiry: models.DateTimeField = models.DateTimeField()
deleted_at: models.DateTimeField = models.DateTimeField(null=True)
@@ -58,6 +59,7 @@ class Meta:
id: models.UUIDField = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
provider: models.CharField = models.CharField(max_length=255)
origin: models.CharField = models.CharField(max_length=255)
+ enterprise: models.BooleanField = models.BooleanField(default=False)
state: models.CharField = models.CharField(max_length=255, db_index=True)
created_at: models.DateTimeField = models.DateTimeField()
expiry: models.DateTimeField = models.DateTimeField()
diff --git a/www/backend/helpar/users/schema.py b/www/backend/helpar/users/schema.py
index 0748735..ecbbfee 100644
--- a/www/backend/helpar/users/schema.py
+++ b/www/backend/helpar/users/schema.py
@@ -25,6 +25,7 @@ def canoncial_phone(self) -> str:
class PKCEAuthRequest(BaseModel):
code: Annotated[str, StringConstraints(min_length=6, max_length=6)]
+ enterprise: bool = False
class UpdateUserRequest(BaseModel):
diff --git a/www/backend/helpar/users/uses.py b/www/backend/helpar/users/uses.py
index 8cc264e..69aea6a 100644
--- a/www/backend/helpar/users/uses.py
+++ b/www/backend/helpar/users/uses.py
@@ -17,7 +17,7 @@
from helpar.lib.utils import *
from helpar.lib.services import AppleAuthentication
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("helpar")
scopes = [
"https://www.googleapis.com/auth/userinfo.profile",
@@ -34,7 +34,7 @@ def _fetch_google_user_info(token: str) -> Optional[Dict[str, Any]]:
return response.json()
-def google_auth(origin: str) -> str:
+def google_auth(origin: str, enterprise: bool) -> str:
mins_expiry = 5
flow = Flow.from_client_config(
client_config={
@@ -55,6 +55,7 @@ def google_auth(origin: str) -> str:
_ = OauthRequest.objects.create(
**{
"provider": "google",
+ "enterprise": enterprise,
"state": params["state"][0],
"created_at": timezone.now(),
"origin": origin,
@@ -140,6 +141,7 @@ def twilio_phone_auth_verification(
if twilio_auth.expiry <= timezone.now():
twilio_auth.deleted_at = timezone.now()
twilio_auth.save()
+ logger.error("Twilio verification code expired.")
return (status.HTTP_400_BAD_REQUEST, {"success": False, "message": "Verification code expired."})
response = httpx.post(
@@ -176,19 +178,33 @@ def twilio_phone_auth_verification(
def pkce_auth(params: PKCEAuthRequest) -> Tuple[status, Dict[str, Any]]:
- pkce_request = PKCERequest.objects.filter(code=params.code, deleted_at__isnull=True).order_by("-created_at").first()
+ from brands.models import Brand
+
+ pkce_request = (
+ PKCERequest.objects.filter(code=params.code, enterprise=params.enterprise, deleted_at__isnull=True)
+ .order_by("-created_at")
+ .first()
+ )
if not pkce_request:
+ logger.error("No PKCE request found for code: %s", params.code)
return (status.HTTP_400_BAD_REQUEST, {"success": False, "message": "Invalid code."})
if pkce_request.expiry <= timezone.now():
+ logger.error("PKCE code %s expired.", params.code)
pkce_request.deleted_at = timezone.now()
pkce_request.save()
return (status.HTTP_400_BAD_REQUEST, {"success": False, "message": "Code expired."})
- data = UserSerializer(pkce_request.user).data
pkce_request.deleted_at = timezone.now()
pkce_request.save()
+ if params.enterprise:
+ brand = Brand.objects.filter(users__id=pkce_request.user.id).first()
+ if not brand:
+ logger.error("User %s not associated with a brand.", pkce_request.user.id)
+ return (status.HTTP_400_BAD_REQUEST, {"success": False, "message": "User not associated with a brand."})
+
+ data = UserSerializer(pkce_request.user).data
return (status.HTTP_200_OK, {"success": True, "user": data})
@@ -216,11 +232,13 @@ def apple_auth_callback(form: AppleAuthForm) -> HttpResponse:
.first()
)
if not oauth_req:
+ logger.error("No oauth request found for state: %s", form.cleaned_data["state"])
return HttpResponseServerError(
content_type="application/json",
content=json.dumps({"success": False, "details": "No oauth request found for state."}),
)
if oauth_req.expiry < timezone.now():
+ logger.error("Apple auth request expired.")
return HttpResponseBadRequest(
content_type="application/json",
content=json.dumps({"success": False, "details": "Apple auth request expired."}),
@@ -233,6 +251,9 @@ def apple_auth_callback(form: AppleAuthForm) -> HttpResponse:
code = random_digits()
expiry = timezone.now() + dt.timedelta(minutes=mins_expiry)
+ path = "d/hq?code=" if oauth_req.enterprise else "m/hq?code="
+ url = oauth_req.origin + path + code
+
if not email or not first_name or not last_name:
derived_email = AppleAuthentication.derive_user_email_with_idcode(form.cleaned_data["id_token"])
if not derived_email:
@@ -244,13 +265,13 @@ def apple_auth_callback(form: AppleAuthForm) -> HttpResponse:
if User.objects.filter(email=derived_email).exists():
user = User.objects.get(email=derived_email)
_ = PKCERequest.objects.create(code=code, expiry=expiry, user_id=user.id, created_at=timezone.now())
- return HttpResponseRedirect(oauth_req.origin + "hq?code=" + code)
+ return HttpResponseRedirect(url)
username = first_name + " " + last_name
if User.objects.filter(email=email, username=username).exists():
user = User.objects.get(email=email, username=username)
_ = PKCERequest.objects.create(code=code, expiry=expiry, user_id=user.id, created_at=timezone.now())
- return HttpResponseRedirect(oauth_req.origin + "hq?code=" + code)
+ return HttpResponseRedirect(url)
user_data = {
"email": email,
@@ -270,7 +291,7 @@ def apple_auth_callback(form: AppleAuthForm) -> HttpResponse:
_ = PKCERequest.objects.create(code=code, expiry=expiry, user_id=user.id, created_at=timezone.now())
- return HttpResponseRedirect(oauth_req.origin + "hq?code=" + code)
+ return HttpResponseRedirect(oauth_req.origin + "m/hq?code=" + code)
def update_user(params: UpdateUserRequest, user_id: str) -> Tuple[status, Dict[str, Any]]:
diff --git a/www/backend/helpar/users/views.py b/www/backend/helpar/users/views.py
index 573bc7d..edd0437 100644
--- a/www/backend/helpar/users/views.py
+++ b/www/backend/helpar/users/views.py
@@ -13,7 +13,7 @@
from t import *
from helpar.lib.utils import *
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("helpar")
@api_view(["PATCH"])
@@ -31,7 +31,8 @@ def update_user_view(request: Request) -> Response:
def google_auth_view(request: Request) -> Response:
try:
origin = request.META.get("HTTP_REFERER", request.META.get("HTTP_ORIGIN"))
- authorization_url = google_auth(origin)
+ enterprise = "enterprise" in request.query_params
+ authorization_url = google_auth(origin, enterprise)
return Response(data={"success": True, "redirect": authorization_url})
except Exception as err:
logger.exception("Google auth error: %s", str(err))
@@ -57,11 +58,11 @@ def google_auth_callback_view(request: Request) -> Response:
)
if rstatus != status.HTTP_200_OK:
- url = oauth_req.origin + "?alert=auth"
+ url = oauth_req.origin + "m?alert=auth"
return HttpResponseRedirect(url)
if oauth_req.expiry < timezone.now():
- url = oauth_req.origin + "?alert=auth"
+ url = oauth_req.origin + "m?alert=auth"
return HttpResponseRedirect(url)
oauth_req.deleted_at = timezone.now()
@@ -71,8 +72,12 @@ def google_auth_callback_view(request: Request) -> Response:
code = random_digits()
expiry = timezone.now() + dt.timedelta(minutes=mins_expiry)
- _ = PKCERequest.objects.create(code=code, expiry=expiry, user_id=user["id"], created_at=timezone.now())
- url = oauth_req.origin + "hq?code=" + code
+ _ = PKCERequest.objects.create(
+ code=code, enterprise=oauth_req.enterprise, expiry=expiry, user_id=user["id"], created_at=timezone.now()
+ )
+
+ path = "m/hq?code=" if not oauth_req.enterprise else "d/dashboard?code="
+ url = oauth_req.origin + path + code
return HttpResponseRedirect(url)
except Exception as err:
logger.exception("Google auth callback error: %s", str(err))
diff --git a/www/frontend/helpar/package.json b/www/frontend/helpar/package.json
index 5566280..2cdbe1c 100644
--- a/www/frontend/helpar/package.json
+++ b/www/frontend/helpar/package.json
@@ -9,40 +9,51 @@
"@radix-ui/react-alert-dialog": "1.0.5",
"@radix-ui/react-avatar": "1.0.4",
"@radix-ui/react-checkbox": "1.0.4",
+ "@radix-ui/react-dropdown-menu": "2.0.6",
"@radix-ui/react-hover-card": "1.0.7",
"@radix-ui/react-icons": "1.3.0",
"@radix-ui/react-label": "2.0.2",
"@radix-ui/react-menubar": "1.0.4",
"@radix-ui/react-navigation-menu": "1.1.4",
+ "@radix-ui/react-popover": "1.0.7",
"@radix-ui/react-progress": "1.0.3",
"@radix-ui/react-radio-group": "1.1.3",
"@radix-ui/react-scroll-area": "1.0.5",
"@radix-ui/react-select": "2.0.0",
+ "@radix-ui/react-separator": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-switch": "1.0.3",
"@radix-ui/react-tabs": "1.0.4",
+ "@radix-ui/react-toast": "1.1.5",
"@radix-ui/react-toggle": "1.0.3",
"@radix-ui/react-toggle-group": "1.0.4",
"@reduxjs/toolkit": "2.0.1",
+ "@types/leaflet": "1.9.8",
+ "add": "2.0.6",
"autoprefixer": "10.4.16",
"axios": "1.6.2",
"class-variance-authority": "0.7.0",
"clsx": "2.0.0",
"cmdk": "0.2.0",
+ "date-fns": "3.0.6",
"eslint": "8.56.0",
+ "leaflet": "1.9.4",
+ "leaflet.heat": "0.2.0",
"lucide-react": "0.294.0",
"postcss": "8.4.32",
"prettier": "3.0.3",
"react": "18.2.0",
- "react-day-picker": "8.9.1",
+ "react-day-picker": "8.10.0",
"react-dom": "18.2.0",
"react-hook-form": "7.49.0",
"react-ionicons": "4.2.1",
+ "react-leaflet": "4.2.1",
"react-redux": "9.0.2",
"react-router-dom": "6.14.2",
"react-scripts": "5.0.1",
"react-slick": "0.29.0",
"react-uuid": "2.0.0",
+ "recharts": "2.10.3",
"slick-carousel": "1.8.1",
"styled-components": "6.1.1",
"swiper": "11.0.5",
@@ -51,6 +62,7 @@
"tailwindcss-animate": "1.0.7",
"validator": "13.11.0",
"web-vitals": "2.1.4",
+ "yarn": "1.22.21",
"zod": "3.22.4"
},
"scripts": {
diff --git a/www/frontend/helpar/src/components/AuthenticationForm.tsx b/www/frontend/helpar/src/components/AuthenticationForm.tsx
new file mode 100644
index 0000000..cf0e5de
--- /dev/null
+++ b/www/frontend/helpar/src/components/AuthenticationForm.tsx
@@ -0,0 +1,71 @@
+import React, { useState } from "react";
+import { useSelector } from "react-redux";
+import { LogoGoogle } from "react-ionicons";
+import { cn } from "../lib/utils";
+import { Button } from "./ui/button";
+import axios from "../services/Axios";
+import Events from "../services/Events";
+import config, { Environment, Module } from "../config";
+import { sanitizeError } from "../utils";
+
+interface UserAuthFormProps extends React.HTMLAttributes {}
+
+const UserAuthForm = ({ className, ...props }: UserAuthFormProps) => {
+ const [loading, setLoading] = useState(false);
+ const [oauthError, setOauthError] = useState(null);
+ const session = useSelector((state: any) => state.user.session);
+ const events = React.useRef(new Events(session));
+
+ const oauthGoogleRequest = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.post(`/users/auth/google/?enterprise=true`);
+
+ if (response.status === 200 && response.data && response.data.redirect) {
+ window.location.href = response.data.redirect;
+ } else {
+ console.error("Failed to authenticate phone number:", response);
+ setOauthError("Failed to authenticate phone number.");
+ setLoading(false);
+ }
+ } catch (error: any) {
+ console.error("Validation failed:", error.toString());
+ setOauthError(sanitizeError(error.toString()));
+ return;
+ }
+ };
+
+ return (
+
+
+
+
+ {oauthError}
+
+
+ );
+};
+
+export default UserAuthForm;
diff --git a/www/frontend/helpar/src/components/dashboard/CustomerType.tsx b/www/frontend/helpar/src/components/dashboard/CustomerType.tsx
new file mode 100644
index 0000000..0bd293e
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/CustomerType.tsx
@@ -0,0 +1,68 @@
+import { PieChart, Pie, Legend, Cell, ResponsiveContainer } from "recharts";
+
+const data = [
+ { name: "Group A", value: 400 },
+ { name: "Group B", value: 300 },
+ { name: "Group C", value: 300 },
+ { name: "Group D", value: 200 },
+];
+const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];
+
+const renderColorfulLegendText = (value: number, entry: any) => {
+ const { color } = entry;
+ return (
+ {`${value} - ${entry.payload.value}`}
+ );
+};
+
+export function CustomerType() {
+ return (
+
+ {}}>
+ {
+ const RADIAN = Math.PI / 180;
+ const radius = innerRadius + (outerRadius - innerRadius) * 2.1;
+ const x = cx + radius * Math.cos(-midAngle * RADIAN);
+ const y = cy + radius * Math.sin(-midAngle * RADIAN);
+
+ return (
+ cx ? "start" : "end"}
+ dominantBaseline="central"
+ style={{ fontSize: 12, fontFamily: "Inter" }}
+ >
+ {`${name}: ${(percent * 100).toFixed(0)}%`}
+
+ );
+ }}
+ >
+ {data.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/DateRangePicker.tsx b/www/frontend/helpar/src/components/dashboard/DateRangePicker.tsx
new file mode 100644
index 0000000..2a58d3c
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/DateRangePicker.tsx
@@ -0,0 +1,68 @@
+import * as React from "react";
+import { CalendarIcon } from "@radix-ui/react-icons";
+import { format } from "date-fns";
+import { DateRange } from "react-day-picker";
+import { cn } from "../../lib/utils";
+import { Button } from "./../ui/button";
+import { Calendar } from "./../ui/calendar";
+import { Popover, PopoverContent, PopoverTrigger } from "./../ui/popover";
+
+interface CalendarDateRangePickerProps {
+ className?: string;
+ onChange: (from: Date, to: Date) => void;
+ startDate: Date;
+ endDate: Date;
+}
+
+export function CalendarDateRangePicker(props: CalendarDateRangePickerProps) {
+ const [date, setDate] = React.useState({
+ from: props.startDate,
+ to: props.endDate,
+ });
+
+ return (
+
+
+
+
+
+
+ {
+ setDate(date);
+ if (date && date.from && date.to) {
+ props.onChange(date.from, date.to);
+ }
+ }}
+ numberOfMonths={2}
+ />
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/MainNav.tsx b/www/frontend/helpar/src/components/dashboard/MainNav.tsx
new file mode 100644
index 0000000..9fa8d01
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/MainNav.tsx
@@ -0,0 +1,40 @@
+import { Link } from "react-router-dom";
+
+import { cn } from "../../lib/utils";
+
+export function MainNav({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/Overview.tsx b/www/frontend/helpar/src/components/dashboard/Overview.tsx
new file mode 100644
index 0000000..8cd953e
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/Overview.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
+
+const data = [
+ {
+ name: "Jan",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Feb",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Mar",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Apr",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "May",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Jun",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Jul",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Aug",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Sep",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Oct",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Nov",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Dec",
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+];
+
+export function Overview() {
+ return (
+
+
+
+ `$${value}`}
+ />
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/RecentCustomers.tsx b/www/frontend/helpar/src/components/dashboard/RecentCustomers.tsx
new file mode 100644
index 0000000..7719356
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/RecentCustomers.tsx
@@ -0,0 +1,63 @@
+import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
+
+const data = [
+ {
+ image: "/avatars/01.png",
+ name: "Olivia Martin",
+ fallback: "OM",
+ email: "olivia.martin@email.com",
+ session_duration: "5m",
+ },
+ {
+ image: "/avatars/02.png",
+ name: "Jackson Lee",
+ fallback: "JL",
+ email: "jackson.lee@gmail.com",
+ session_duration: "2m",
+ },
+ {
+ image: "/avatars/03.png",
+ name: "Isabella Nguyen",
+ fallback: "IN",
+ email: "isabella.nguyen@email.com",
+ session_duration: "3m",
+ },
+ {
+ image: "/avatars/04.png",
+ name: "William Kim",
+ fallback: "WK",
+ email: "will@email.com",
+ session_duration: "1m",
+ },
+ {
+ image: "/avatars/05.png",
+ name: "Sofia Davis",
+ fallback: "SD",
+ email: "sofiad123@email.com",
+ session_duration: "2m",
+ },
+];
+
+export function RecentCustomers() {
+ return (
+
+
+
Name
+
Session Duration
+
+ {data.map((customer, index) => (
+
+
+
+ {customer.fallback}
+
+
+
{customer.name}
+
{customer.email}
+
+
{customer.session_duration}
+
+ ))}
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/Search.tsx b/www/frontend/helpar/src/components/dashboard/Search.tsx
new file mode 100644
index 0000000..84be2ac
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/Search.tsx
@@ -0,0 +1,13 @@
+import { Input } from "./../ui/input";
+
+export function Search() {
+ return (
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/SessionLocation.tsx b/www/frontend/helpar/src/components/dashboard/SessionLocation.tsx
new file mode 100644
index 0000000..4c54f7e
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/SessionLocation.tsx
@@ -0,0 +1,78 @@
+import { useEffect } from "react";
+import { MapContainer, TileLayer, useMap } from "react-leaflet";
+import L from "leaflet";
+import "leaflet/dist/leaflet.css";
+import "leaflet.heat";
+
+type Location = {
+ lat: number;
+ lng: number;
+ value: number;
+};
+
+const data: Location[] = [
+ { lat: 40.7128, lng: -74.006, value: 491 },
+ { lat: 34.0522, lng: -118.2437, value: 153 },
+ { lat: 41.8781, lng: -87.6298, value: 112 },
+ { lat: 29.7604, lng: -95.3698, value: 148 },
+ { lat: 33.4484, lng: -112.074, value: 265 },
+ { lat: 39.9526, lng: -75.1652, value: 388 },
+ { lat: 29.4241, lng: -98.4936, value: 146 },
+ { lat: 32.7157, lng: -117.1611, value: 424 },
+ { lat: 32.7767, lng: -96.797, value: 398 },
+ { lat: 37.3382, lng: -121.8863, value: 290 },
+ { lat: 30.2672, lng: -97.7431, value: 491 },
+ { lat: 30.3322, lng: -81.6557, value: 341 },
+ { lat: 32.7555, lng: -97.3308, value: 349 },
+ { lat: 39.9612, lng: -82.9988, value: 209 },
+ { lat: 35.2271, lng: -80.8431, value: 279 },
+ { lat: 37.7749, lng: -122.4194, value: 419 },
+ { lat: 39.7684, lng: -86.1581, value: 419 },
+ { lat: 47.6062, lng: -122.3321, value: 301 },
+ { lat: 39.7392, lng: -104.9903, value: 126 },
+ { lat: 38.9072, lng: -77.0369, value: 387 },
+];
+const HeatmapLayer = () => {
+ const map = useMap();
+
+ useEffect(() => {
+ const heatPoints = data.map((location) => [
+ location.lat,
+ location.lng,
+ location.value,
+ ]);
+ const heatLayer = (L as any)
+ .heatLayer(heatPoints, {
+ radius: 20,
+ minOpacity: 0.4,
+ blur: 0,
+ max: 500,
+ gradient: {
+ 0.2: "yellow",
+ 0.5: "orange",
+ 0.8: "red",
+ 1.0: "darkred",
+ },
+ })
+ .addTo(map);
+ return () => {
+ map.removeLayer(heatLayer);
+ };
+ }, [map]);
+
+ return null;
+};
+
+export function SessionLocation() {
+ return (
+ // @ts-ignore
+
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/SidebarNav.tsx b/www/frontend/helpar/src/components/dashboard/SidebarNav.tsx
new file mode 100644
index 0000000..b31b404
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/SidebarNav.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { cn } from "../../lib/utils";
+import { Button } from "../ui/button";
+
+export type NavbarItem = "Profile" | "Account" | "Notifications" | "Billing";
+
+interface SidebarNavProps extends React.HTMLAttributes {
+ items: {
+ title: string;
+ }[];
+ currentNavbarItem: NavbarItem;
+ onItemSelect: (item: NavbarItem) => void;
+}
+
+const SidebarNav = (props: SidebarNavProps): React.ReactElement => {
+ return (
+
+ );
+};
+
+export default SidebarNav;
diff --git a/www/frontend/helpar/src/components/dashboard/SocialMedia.tsx b/www/frontend/helpar/src/components/dashboard/SocialMedia.tsx
new file mode 100644
index 0000000..f63d378
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/SocialMedia.tsx
@@ -0,0 +1,70 @@
+import { PieChart, Pie, Legend, Cell, ResponsiveContainer } from "recharts";
+
+const data = [
+ { name: "Facebook", value: 400 },
+ { name: "Twitter", value: 300 },
+ { name: "Instagram", value: 300 },
+ { name: "TikTok", value: 200 },
+];
+const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];
+
+const renderColorfulLegendText = (value: number, entry: any) => {
+ const { color } = entry;
+ return (
+ {`${value} - ${entry.payload.value}`}
+ );
+};
+
+const renderCustomizedLabel = ({
+ cx,
+ cy,
+ midAngle,
+ innerRadius,
+ outerRadius,
+ percent,
+ index,
+ name,
+}: any) => {
+ const radius = innerRadius + (outerRadius - innerRadius) * 0.4;
+ const x = cx + radius * Math.cos((-midAngle * Math.PI) / 180);
+ const y = cy + radius * Math.sin((-midAngle * Math.PI) / 180);
+
+ return (
+ cx ? "start" : "end"}
+ dominantBaseline="central"
+ style={{ fontSize: 12, fontFamily: "Inter" }}
+ >
+ {`${name}: ${(percent * 100).toFixed(0)}%`}
+
+ );
+};
+
+export function SocialMedia() {
+ return (
+
+
+
+ {data.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/Summary.tsx b/www/frontend/helpar/src/components/dashboard/Summary.tsx
new file mode 100644
index 0000000..caf4afc
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/Summary.tsx
@@ -0,0 +1,152 @@
+"use client";
+
+import {
+ Line,
+ LineChart,
+ CartesianGrid,
+ Legend,
+ ResponsiveContainer,
+ XAxis,
+ YAxis,
+} from "recharts";
+import { shortFormat } from "../../utils";
+
+const data = [
+ {
+ name: "Jan",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Feb",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Mar",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Apr",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "May",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Jun",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Jul",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Aug",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Sep",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Oct",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Nov",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: "Dec",
+ sessions: Math.floor(Math.random() * 5000) + 1000,
+ engagement: Math.floor(Math.random() * 5000) + 1000,
+ },
+];
+
+const renderColorfulLegendText = (value: string) => {
+ value = value[0].toUpperCase() + value.slice(1);
+ return {value};
+};
+
+export function Summary() {
+ return (
+
+
+
+ shortFormat(value)}
+ />
+ shortFormat(value)}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/SupportTopics.tsx b/www/frontend/helpar/src/components/dashboard/SupportTopics.tsx
new file mode 100644
index 0000000..49c8794
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/SupportTopics.tsx
@@ -0,0 +1,44 @@
+import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from "recharts";
+
+const data = [
+ { topic: "returns", value: 10 },
+ { topic: "reorders", value: 37 },
+ { topic: "getting started", value: 110 },
+ { topic: "customer support", value: 20 },
+ { topic: "refund", value: 55 },
+ { topic: "shower head", value: 100 },
+ { topic: "support", value: 55 },
+ { topic: "tiny aligator", value: 3 },
+];
+
+export function SupportTopics() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/TeamSwitcher.tsx b/www/frontend/helpar/src/components/dashboard/TeamSwitcher.tsx
new file mode 100644
index 0000000..42afb89
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/TeamSwitcher.tsx
@@ -0,0 +1,208 @@
+"use client";
+
+import * as React from "react";
+import {
+ CaretSortIcon,
+ CheckIcon,
+ PlusCircledIcon,
+} from "@radix-ui/react-icons";
+
+import { cn } from "./../../lib/utils";
+import { Avatar, AvatarFallback, AvatarImage } from "./../ui/avatar";
+import { Button } from "./../ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from "./../ui/command";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "./../ui/dialog";
+import { Input } from "./../ui/input";
+import { Label } from "./../ui/label";
+import { Popover, PopoverContent, PopoverTrigger } from "./../ui/popover";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "./../ui/select";
+
+const groups = [
+ {
+ label: "Personal Account",
+ teams: [
+ {
+ label: "Alicia Koch",
+ value: "personal",
+ },
+ ],
+ },
+ {
+ label: "Teams",
+ teams: [
+ {
+ label: "Acme Inc.",
+ value: "acme-inc",
+ },
+ {
+ label: "Monsters Inc.",
+ value: "monsters",
+ },
+ ],
+ },
+];
+
+type Team = (typeof groups)[number]["teams"][number];
+
+type PopoverTriggerProps = React.ComponentPropsWithoutRef<
+ typeof PopoverTrigger
+>;
+
+interface TeamSwitcherProps extends PopoverTriggerProps {}
+
+export default function TeamSwitcher({ className }: TeamSwitcherProps) {
+ const [open, setOpen] = React.useState(false);
+ const [showNewTeamDialog, setShowNewTeamDialog] = React.useState(false);
+ const [selectedTeam, setSelectedTeam] = React.useState(
+ groups[0].teams[0],
+ );
+
+ return (
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/dashboard/TopNavigation.tsx b/www/frontend/helpar/src/components/dashboard/TopNavigation.tsx
new file mode 100644
index 0000000..0634637
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/TopNavigation.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import TeamSwitcher from "./TeamSwitcher";
+import { MainNav } from "./MainNav";
+import { Search } from "./Search";
+import { UserNav } from "./UserNav";
+
+const TopNavigation = (): React.JSX.Element => {
+ return (
+
+ );
+};
+
+export default TopNavigation;
diff --git a/www/frontend/helpar/src/components/dashboard/UserNav.tsx b/www/frontend/helpar/src/components/dashboard/UserNav.tsx
new file mode 100644
index 0000000..58f2e31
--- /dev/null
+++ b/www/frontend/helpar/src/components/dashboard/UserNav.tsx
@@ -0,0 +1,60 @@
+import { Avatar, AvatarFallback, AvatarImage } from "./../ui/avatar";
+import { useSelector } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import { Button } from "./../ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "./../ui/dropdown-menu";
+import { Session } from "../../services/Session";
+
+export function UserNav() {
+ const navigate = useNavigate();
+ const user = useSelector((state: any) => state.user.session);
+ return (
+
+
+
+
+
+
+
+
shadcn
+
+ {user.user?.email}
+
+
+
+
+
+ navigate("/d/settings")}
+ >
+ Settings
+
+
+ Billing
+
+
+
+ Session.logout()}
+ >
+ Log out
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/forms/AccountForm.tsx b/www/frontend/helpar/src/components/forms/AccountForm.tsx
new file mode 100644
index 0000000..f9a3920
--- /dev/null
+++ b/www/frontend/helpar/src/components/forms/AccountForm.tsx
@@ -0,0 +1,208 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { CalendarIcon, CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
+import { format } from "date-fns";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { cn } from "../../lib/utils";
+import { Button } from "../ui/button";
+import { Calendar } from "../ui/calendar";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+} from "../ui/command";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../ui/form";
+import { Input } from "../ui/input";
+import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
+import { Toast } from "../ui/toast";
+
+const languages = [
+ { label: "English", value: "en" },
+ { label: "French", value: "fr" },
+ { label: "German", value: "de" },
+ { label: "Spanish", value: "es" },
+ { label: "Portuguese", value: "pt" },
+ { label: "Russian", value: "ru" },
+ { label: "Japanese", value: "ja" },
+ { label: "Korean", value: "ko" },
+ { label: "Chinese", value: "zh" },
+] as const;
+
+const accountFormSchema = z.object({
+ name: z
+ .string()
+ .min(2, {
+ message: "Name must be at least 2 characters.",
+ })
+ .max(30, {
+ message: "Name must not be longer than 30 characters.",
+ }),
+ dob: z.date({
+ required_error: "A date of birth is required.",
+ }),
+ language: z.string({
+ required_error: "Please select a language.",
+ }),
+});
+
+type AccountFormValues = z.infer;
+
+// This can come from your database or API.
+const defaultValues: Partial = {
+ // name: "Your name",
+ // dob: new Date("2023-01-23"),
+};
+
+export function AccountForm() {
+ const form = useForm({
+ resolver: zodResolver(accountFormSchema),
+ defaultValues,
+ });
+
+ function onSubmit(data: AccountFormValues) {
+ Toast({
+ title: "You submitted the form",
+ });
+ }
+
+ return (
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/forms/EnterpriseProfileForm.tsx b/www/frontend/helpar/src/components/forms/EnterpriseProfileForm.tsx
new file mode 100644
index 0000000..32423a2
--- /dev/null
+++ b/www/frontend/helpar/src/components/forms/EnterpriseProfileForm.tsx
@@ -0,0 +1,186 @@
+"use client";
+
+import { Link } from "react-router-dom";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useFieldArray, useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { cn } from "./../../lib/utils";
+import { Button } from "./../../components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "./../../components/ui/form";
+import { Input } from "./../../components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "./../../components/ui/select";
+import { Textarea } from "./../../components/ui/textarea";
+import { Toast } from "./../../components/ui/toast";
+
+const profileFormSchema = z.object({
+ username: z
+ .string()
+ .min(2, {
+ message: "Username must be at least 2 characters.",
+ })
+ .max(30, {
+ message: "Username must not be longer than 30 characters.",
+ }),
+ email: z
+ .string({
+ required_error: "Please select an email to display.",
+ })
+ .email(),
+ bio: z.string().max(160).min(4),
+ urls: z
+ .array(
+ z.object({
+ value: z.string().url({ message: "Please enter a valid URL." }),
+ }),
+ )
+ .optional(),
+});
+
+type ProfileFormValues = z.infer;
+
+// This can come from your database or API.
+const defaultValues: Partial = {
+ bio: "I own a computer.",
+ urls: [
+ { value: "https://shadcn.com" },
+ { value: "http://twitter.com/shadcn" },
+ ],
+};
+
+export function EnterpriseProfileForm() {
+ const form = useForm({
+ resolver: zodResolver(profileFormSchema),
+ defaultValues,
+ mode: "onChange",
+ });
+
+ const { fields, append } = useFieldArray({
+ name: "urls",
+ control: form.control,
+ });
+
+ function onSubmit(data: ProfileFormValues) {
+ Toast({
+ title: "You submitted the following values:",
+ });
+ }
+
+ return (
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/soom/SurveyResult.tsx b/www/frontend/helpar/src/components/soom/SurveyResult.tsx
index 6aac5dc..7840ac1 100644
--- a/www/frontend/helpar/src/components/soom/SurveyResult.tsx
+++ b/www/frontend/helpar/src/components/soom/SurveyResult.tsx
@@ -2,7 +2,7 @@ import React, { useState } from "react";
import PopupCard from "../../components/soom/PopupCardSurveyResult";
interface SurveyNameProps {
- onNextClick: () => void;
+ onNextClick: () => void;
}
const SurveyBirthday: React.FC = ({
@@ -11,7 +11,7 @@ const SurveyBirthday: React.FC = ({
const [isPopupUp, setIsPopupUp] = useState(false);
const handlePopupCardClick = () => {
- setIsPopupUp(!isPopupUp);
+ setIsPopupUp(!isPopupUp);
};
return (
@@ -43,8 +43,6 @@ const SurveyBirthday: React.FC = ({
Our secret garden of rest
-
-
= ({
buttonLink="https://soomshower.com/products/aqua-purifying-filter?variant=44219720106234"
onPopupCardClick={handlePopupCardClick}
/>
-
-
);
};
diff --git a/www/frontend/helpar/src/components/ui/command.tsx b/www/frontend/helpar/src/components/ui/command.tsx
new file mode 100644
index 0000000..e8990f1
--- /dev/null
+++ b/www/frontend/helpar/src/components/ui/command.tsx
@@ -0,0 +1,155 @@
+"use client";
+
+import * as React from "react";
+import { type DialogProps } from "@radix-ui/react-dialog";
+import { Command as CommandPrimitive } from "cmdk";
+import { Search } from "lucide-react";
+
+import { cn } from "../../lib/utils";
+import { Dialog, DialogContent } from "./dialog";
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Command.displayName = CommandPrimitive.displayName;
+
+interface CommandDialogProps extends DialogProps {}
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+
+ );
+};
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+
+CommandInput.displayName = CommandPrimitive.Input.displayName;
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+
+CommandList.displayName = CommandPrimitive.List.displayName;
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+));
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName;
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+
+CommandItem.displayName = CommandPrimitive.Item.displayName;
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+CommandShortcut.displayName = "CommandShortcut";
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+};
diff --git a/www/frontend/helpar/src/components/ui/dropdown-menu.tsx b/www/frontend/helpar/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..a966468
--- /dev/null
+++ b/www/frontend/helpar/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,200 @@
+"use client";
+
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+
+import { cn } from "../../lib/utils";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/www/frontend/helpar/src/components/ui/popover.tsx b/www/frontend/helpar/src/components/ui/popover.tsx
new file mode 100644
index 0000000..4c80194
--- /dev/null
+++ b/www/frontend/helpar/src/components/ui/popover.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import * as React from "react";
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+
+import { cn } from "../../lib/utils";
+
+const Popover = PopoverPrimitive.Root;
+
+const PopoverTrigger = PopoverPrimitive.Trigger;
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
+
+export { Popover, PopoverTrigger, PopoverContent };
diff --git a/www/frontend/helpar/src/components/ui/separator.tsx b/www/frontend/helpar/src/components/ui/separator.tsx
new file mode 100644
index 0000000..3d74e5b
--- /dev/null
+++ b/www/frontend/helpar/src/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import * as React from "react";
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+
+import { cn } from "../../lib/utils";
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref,
+ ) => (
+
+ ),
+);
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/www/frontend/helpar/src/components/ui/toast.tsx b/www/frontend/helpar/src/components/ui/toast.tsx
new file mode 100644
index 0000000..b37cd3e
--- /dev/null
+++ b/www/frontend/helpar/src/components/ui/toast.tsx
@@ -0,0 +1,127 @@
+import * as React from "react";
+import * as ToastPrimitives from "@radix-ui/react-toast";
+import { cva, type VariantProps } from "class-variance-authority";
+import { X } from "lucide-react";
+
+import { cn } from "../../lib/utils";
+
+const ToastProvider = ToastPrimitives.Provider;
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ );
+});
+Toast.displayName = ToastPrimitives.Root.displayName;
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastAction.displayName = ToastPrimitives.Action.displayName;
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+ToastClose.displayName = ToastPrimitives.Close.displayName;
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastTitle.displayName = ToastPrimitives.Title.displayName;
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastDescription.displayName = ToastPrimitives.Description.displayName;
+
+type ToastProps = React.ComponentPropsWithoutRef;
+
+type ToastActionElement = React.ReactElement;
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+};
diff --git a/www/frontend/helpar/src/components/ui/toaster.tsx b/www/frontend/helpar/src/components/ui/toaster.tsx
new file mode 100644
index 0000000..d38613a
--- /dev/null
+++ b/www/frontend/helpar/src/components/ui/toaster.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import {
+ Toast,
+ ToastClose,
+ ToastDescription,
+ ToastProvider,
+ ToastTitle,
+ ToastViewport,
+} from "./toast";
+import { useToast } from "./use-toast";
+
+export function Toaster() {
+ const { toasts } = useToast();
+
+ return (
+
+ {toasts.map(function ({ id, title, description, action, ...props }) {
+ return (
+
+
+ {title && {title}}
+ {description && (
+ {description}
+ )}
+
+ {action}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/components/ui/use-toast.tsx b/www/frontend/helpar/src/components/ui/use-toast.tsx
new file mode 100644
index 0000000..5d36f2f
--- /dev/null
+++ b/www/frontend/helpar/src/components/ui/use-toast.tsx
@@ -0,0 +1,189 @@
+// Inspired by react-hot-toast library
+import * as React from "react";
+
+import type { ToastActionElement, ToastProps } from "./toast";
+
+const TOAST_LIMIT = 1;
+const TOAST_REMOVE_DELAY = 1000000;
+
+type ToasterToast = ToastProps & {
+ id: string;
+ title?: React.ReactNode;
+ description?: React.ReactNode;
+ action?: ToastActionElement;
+};
+
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const;
+
+let count = 0;
+
+function genId() {
+ count = (count + 1) % Number.MAX_SAFE_INTEGER;
+ return count.toString();
+}
+
+type ActionType = typeof actionTypes;
+
+type Action =
+ | {
+ type: ActionType["ADD_TOAST"];
+ toast: ToasterToast;
+ }
+ | {
+ type: ActionType["UPDATE_TOAST"];
+ toast: Partial;
+ }
+ | {
+ type: ActionType["DISMISS_TOAST"];
+ toastId?: ToasterToast["id"];
+ }
+ | {
+ type: ActionType["REMOVE_TOAST"];
+ toastId?: ToasterToast["id"];
+ };
+
+interface State {
+ toasts: ToasterToast[];
+}
+
+const toastTimeouts = new Map>();
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return;
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId);
+ dispatch({
+ type: "REMOVE_TOAST",
+ toastId: toastId,
+ });
+ }, TOAST_REMOVE_DELAY);
+
+ toastTimeouts.set(toastId, timeout);
+};
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ };
+
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t,
+ ),
+ };
+
+ case "DISMISS_TOAST": {
+ const { toastId } = action;
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId);
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id);
+ });
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t,
+ ),
+ };
+ }
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ };
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ };
+ }
+};
+
+const listeners: Array<(state: State) => void> = [];
+
+let memoryState: State = { toasts: [] };
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action);
+ listeners.forEach((listener) => {
+ listener(memoryState);
+ });
+}
+
+type Toast = Omit;
+
+function toast({ ...props }: Toast) {
+ const id = genId();
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: "UPDATE_TOAST",
+ toast: { ...props, id },
+ });
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
+
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss();
+ },
+ },
+ });
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ };
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState);
+
+ React.useEffect(() => {
+ listeners.push(setState);
+ return () => {
+ const index = listeners.indexOf(setState);
+ if (index > -1) {
+ listeners.splice(index, 1);
+ }
+ };
+ }, [state]);
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ };
+}
+
+export { useToast, toast };
diff --git a/www/frontend/helpar/src/index.css b/www/frontend/helpar/src/index.css
index ed024cb..4fdd00a 100644
--- a/www/frontend/helpar/src/index.css
+++ b/www/frontend/helpar/src/index.css
@@ -59,3 +59,15 @@
height: 1.4rem;
width: 1.4rem;
}
+
+.scrollable-content {
+ max-height: 800px;
+ overflow-y: auto;
+}
+
+/* box shadow accept on the bottom */
+.box-shadow-nb {
+ box-shadow:
+ 0px -2px 4px rgba(0, 0, 0, 0.1),
+ 0px 4px 4px rgba(0, 0, 0, 0.1);
+}
diff --git a/www/frontend/helpar/src/routes/index.tsx b/www/frontend/helpar/src/routes/index.tsx
index fbd8d10..847d803 100644
--- a/www/frontend/helpar/src/routes/index.tsx
+++ b/www/frontend/helpar/src/routes/index.tsx
@@ -1,13 +1,18 @@
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
-import HomeView from "../views/Home";
-import AuthenticateView from "../views/Authenticate";
-import HQ from "../views/HQ";
-import ChatView from "../views/Chat";
-import ConfirmCode from "../views/ConfirmCode";
-import SupportView from "../views/Support";
-import AboutView from "../views/About";
+import AboutView from "../views/user/About";
+import UserAuthenticate from "../views/user/Authenticate";
+import ChatView from "../views/user/Chat";
+import ConfirmCode from "../views/user/ConfirmCode";
+import EnterpriseHome from "../views/enterprise/Index";
+import EnterpriseAuthenticate from "../views/enterprise/Authenticate";
+import EnterpriseSettings from "../views/enterprise/Settings";
+import Dashboard from "../views/enterprise/Dashboard";
+import HQ from "../views/user/HQ";
+import Index from "../views/Index";
+import SupportView from "../views/user/Support";
+import UserHome from "../views/user/Index";
import SoomRouter from "./soom";
@@ -15,14 +20,19 @@ const AppRouter = (): React.JSX.Element => {
return (
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
);
diff --git a/www/frontend/helpar/src/routes/soom.tsx b/www/frontend/helpar/src/routes/soom.tsx
index 6850904..a87cc24 100644
--- a/www/frontend/helpar/src/routes/soom.tsx
+++ b/www/frontend/helpar/src/routes/soom.tsx
@@ -1,20 +1,20 @@
import React from "react";
import { Routes, Route } from "react-router-dom";
-import GettingStarted from "../views/soom/GettingStarted";
-import Offer from "../views/soom/Offer";
+import GettingStarted from "../views/user/soom/GettingStarted";
+import Offer from "../views/user/soom/Offer";
-import WallHQ from "../views/soom/WallHQ";
-import HandHQ from "../views/soom/HandHQ";
-import Reorder from "../views/soom/Reorder";
+import WallHQ from "../views/user/soom/WallHQ";
+import HandHQ from "../views/user/soom/HandHQ";
+import Reorder from "../views/user/soom/Reorder";
-import WallFilter from "../views/soom/wall/WallFilter";
-import WallMount from "../views/soom/wall/WallMount";
-import WallPods from "../views/soom/wall/WallPods";
+import WallFilter from "../views/user/soom/wall/WallFilter";
+import WallMount from "../views/user/soom/wall/WallMount";
+import WallPods from "../views/user/soom/wall/WallPods";
-import HandFilter from "../views/soom/hand/HandFilter";
-import HandMount from "../views/soom/hand/HandMount";
-import HandPods from "../views/soom/hand/HandPods";
+import HandFilter from "../views/user/soom/hand/HandFilter";
+import HandMount from "../views/user/soom/hand/HandMount";
+import HandPods from "../views/user/soom/hand/HandPods";
import "../styles/route.css";
diff --git a/www/frontend/helpar/src/services/Authentication.tsx b/www/frontend/helpar/src/services/Authentication.tsx
new file mode 100644
index 0000000..c9f1404
--- /dev/null
+++ b/www/frontend/helpar/src/services/Authentication.tsx
@@ -0,0 +1,39 @@
+import { useDispatch } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import axios from "../services/Axios";
+import { Session } from "../services/Session";
+import { ActionType } from "../store/reducers/user";
+
+const useAuthentication = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const pkceAuth = async (code: string, enterprise: boolean) => {
+ const path = enterprise ? "/d" : "/m";
+ try {
+ const response = await axios.post(`/users/auth/pkce/`, {
+ code,
+ enterprise,
+ });
+
+ if (response.status === 200 && response.data && response.data.success) {
+ const session = Session.fromUser(response.data.user);
+ console.log("Successfully authenticated via pkce.");
+ dispatch({
+ type: ActionType.SET_SESSION,
+ value: session,
+ });
+ } else {
+ console.error("Failed to authenticate:", response);
+ return navigate(`${path}?alert=auth`);
+ }
+ } catch (error: any) {
+ console.error("Validation failed:", error.toString());
+ return navigate(`${path}?alert=auth`);
+ }
+ };
+
+ return { pkceAuth };
+};
+
+export default useAuthentication;
diff --git a/www/frontend/helpar/src/services/Brand.tsx b/www/frontend/helpar/src/services/Brand.tsx
new file mode 100644
index 0000000..578faa2
--- /dev/null
+++ b/www/frontend/helpar/src/services/Brand.tsx
@@ -0,0 +1,65 @@
+import { useDispatch, useSelector } from "react-redux";
+import Brand from "../models/brands/Brand";
+import HQ from "../models/brands/HQ";
+import axios from "../services/Axios";
+import { CachedBrand, CachedHQ } from "../services/Session";
+import { ActionType } from "../store/reducers/brand";
+import { deriveCachedBrandName } from "../utils";
+
+const useBrandService = () => {
+ const dispatch = useDispatch();
+ const brand = useSelector((state: any) => state.brand.brand);
+ const session = useSelector((state: any) => state.user.session);
+
+ const fetchBrandInfo = async () => {
+ const canonical = deriveCachedBrandName();
+ try {
+ const response = await axios.get(
+ `/brands/canonical-brand-info/${canonical}/`,
+ );
+ if (response.status === 200 && response.data && response.data.data) {
+ const cachedBrand = new CachedBrand(
+ Brand.fromJSON(response.data.data.brand),
+ );
+ cachedBrand.save();
+ dispatch({
+ type: ActionType.SET_BRAND,
+ value: cachedBrand,
+ });
+ console.log("Successfully fetched brand info.");
+ } else {
+ console.error("Failed to fetch brand:", response);
+ }
+ } catch (error: any) {
+ console.error("Failed to fetch brand:", error.toString());
+ }
+ };
+
+ const fetchHQInfo = async () => {
+ if (brand && session) {
+ try {
+ const response = await axios.get(`/brands/${brand.brand.id}/hq/`, {
+ headers: { Authorization: `Bearer ${session.user.jwt}` },
+ });
+ if (response.status === 200 && response.data && response.data.data) {
+ const cachedHQ = new CachedHQ(
+ HQ.fromJSON(response.data.data),
+ Brand.fromJSON(response.data.data.brand),
+ );
+ cachedHQ.save();
+ dispatch({
+ type: ActionType.SET_HQ,
+ value: cachedHQ,
+ });
+ console.log("Successfully fetched HQ info.");
+ }
+ } catch (err: any) {
+ console.error(`Could not fetch HQ info:`, err.message);
+ }
+ }
+ };
+
+ return { fetchBrandInfo, fetchHQInfo };
+};
+
+export default useBrandService;
diff --git a/www/frontend/helpar/src/services/Dashboard.tsx b/www/frontend/helpar/src/services/Dashboard.tsx
new file mode 100644
index 0000000..6e5d913
--- /dev/null
+++ b/www/frontend/helpar/src/services/Dashboard.tsx
@@ -0,0 +1,42 @@
+import { useCallback } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import axios from "../services/Axios";
+import { ActionType } from "../store/reducers/dashboard";
+import { simpleDateFormat } from "../utils";
+
+const useDashboardService = () => {
+ const dispatch = useDispatch();
+ const brand = useSelector((state: any) => state.brand.brand);
+ const session = useSelector((state: any) => state.user.session);
+
+ const fetchDashboardData = useCallback(
+ async (startDate: Date, endDate: Date, setError: any) => {
+ const start = simpleDateFormat(startDate);
+ const end = simpleDateFormat(endDate);
+ try {
+ const response = await axios.get(
+ `/brands/${brand.brand?.id}/dashboard/?start_date=${start}&end_date=${end}`,
+ {
+ headers: {
+ Authorization: `Bearer ${session.jwt()}`,
+ },
+ },
+ );
+
+ if (response.status === 200 && response.data && response.data.success) {
+ dispatch({ type: ActionType.SET_DATA, value: response.data.data });
+ } else {
+ setError("Failed to fetch dashboard data.");
+ }
+ } catch (err: any) {
+ console.error(`Failed to fetch dashboard data.: ${err.toString()}`);
+ setError(err.toString());
+ }
+ },
+ [brand, dispatch, session],
+ );
+
+ return { fetchDashboardData };
+};
+
+export default useDashboardService;
diff --git a/www/frontend/helpar/src/services/Session.tsx b/www/frontend/helpar/src/services/Session.tsx
index c87f6ba..cf5a813 100644
--- a/www/frontend/helpar/src/services/Session.tsx
+++ b/www/frontend/helpar/src/services/Session.tsx
@@ -3,8 +3,8 @@ import Brand from "../models/brands/Brand";
import HQ from "../models/brands/HQ";
import { TUser } from "../models/users/User";
-const SESSION_STORAGE_KEY = "x-helpar-user";
-const BRAND_SESSION_STORAGE_KEY = "x-helpar-brand";
+const USER_STORAGE_KEY = "x-helpar-user";
+const BRAND_USER_STORAGE_KEY = "x-helpar-brand";
const HQ_SESSION_KEY = "x-helpar-hq";
abstract class CacheItem {
@@ -23,7 +23,7 @@ export class Session implements CacheItem {
}
static load(): Session {
- const user = localStorage.getItem(SESSION_STORAGE_KEY);
+ const user = localStorage.getItem(USER_STORAGE_KEY);
if (user) {
return new Session(User.fromJSON(JSON.parse(user)));
}
@@ -34,6 +34,10 @@ export class Session implements CacheItem {
return this.user?.username as string;
}
+ firstName(): string {
+ return this.user?.username.split(" ")[0] as string;
+ }
+
phone(): string {
return this.user?.phone as string;
}
@@ -55,10 +59,7 @@ export class Session implements CacheItem {
}
save() {
- localStorage.setItem(
- SESSION_STORAGE_KEY,
- JSON.stringify(this.user?.toJSON()),
- );
+ localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(this.user?.toJSON()));
}
static fromUser(data: TUser): Session {
@@ -67,23 +68,25 @@ export class Session implements CacheItem {
session.save();
return session;
}
+
+ static logout() {
+ localStorage.removeItem(USER_STORAGE_KEY);
+ window.location.href = "/d";
+ }
}
// NOTE: This isn't technically a 'session', but it uses a few similar
-// session APIs so just lumping it in here for now.
+// session APIs so just lumping it in here for now. Maybe these should be
+// moved to models.
export class CachedBrand implements CacheItem {
- private _brand: Nullable;
+ brand: Nullable;
constructor(brand: Nullable = null) {
- this._brand = brand;
- }
-
- brand(): Nullable {
- return this._brand;
+ this.brand = brand;
}
static load(): CachedBrand {
- const brand = localStorage.getItem(BRAND_SESSION_STORAGE_KEY);
+ const brand = localStorage.getItem(BRAND_USER_STORAGE_KEY);
if (brand) {
return new CachedBrand(Brand.fromJSON(JSON.parse(brand)));
}
@@ -91,30 +94,27 @@ export class CachedBrand implements CacheItem {
}
isValid() {
- return !!this.brand && !!this._brand?.id;
+ return !!this.brand && !!this.brand?.id;
}
save() {
localStorage.setItem(
- BRAND_SESSION_STORAGE_KEY,
- JSON.stringify(this._brand?.toJSON()),
+ BRAND_USER_STORAGE_KEY,
+ JSON.stringify(this.brand?.toJSON()),
);
}
}
// NOTE: This isn't technically a 'session', but it uses a few similar
-// session APIs so just lumping it in here for now.
+// session APIs so just lumping it in here for now. Maybe these should be
+// moved to models.
export class CachedHQ implements CacheItem {
- private _hq: Nullable;
- private _brand: Nullable;
+ hq: Nullable;
+ brand: Nullable;
constructor(hq: Nullable = null, brand: Nullable = null) {
- this._hq = hq;
- this._brand = brand;
- }
-
- hq(): HQ {
- return this._hq as HQ;
+ this.hq = hq;
+ this.brand = brand;
}
static load(): CachedHQ {
@@ -127,10 +127,10 @@ export class CachedHQ implements CacheItem {
}
isValid() {
- return !!this._hq && !!this._hq.id && this._brand?.id === this._hq.brand.id;
+ return !!this.hq && !!this.hq.id && this.brand?.id === this.hq.brand.id;
}
save() {
- localStorage.setItem(HQ_SESSION_KEY, JSON.stringify(this._hq?.toJSON()));
+ localStorage.setItem(HQ_SESSION_KEY, JSON.stringify(this.hq?.toJSON()));
}
}
diff --git a/www/frontend/helpar/src/services/index.tsx b/www/frontend/helpar/src/services/index.tsx
new file mode 100644
index 0000000..3dcae90
--- /dev/null
+++ b/www/frontend/helpar/src/services/index.tsx
@@ -0,0 +1,5 @@
+import useBrandService from "./Brand";
+import useAuthenticationService from "./Authentication";
+import useDashboardService from "./Dashboard";
+
+export { useBrandService, useAuthenticationService, useDashboardService };
diff --git a/www/frontend/helpar/src/store/index.tsx b/www/frontend/helpar/src/store/index.tsx
index dee8f1b..1451400 100644
--- a/www/frontend/helpar/src/store/index.tsx
+++ b/www/frontend/helpar/src/store/index.tsx
@@ -1,13 +1,26 @@
import { configureStore } from "@reduxjs/toolkit";
-import { setHand, setWall } from "./reducers/soom";
+import { soomHandState, soomWallState } from "./reducers/soom";
+import { dashboardState } from "./reducers/dashboard";
+import { brandState } from "./reducers/brand";
+import { userState } from "./reducers/user";
const store = configureStore({
reducer: {
// @ts-ignore
- soomSetHand: setHand,
+ soomHand: soomHandState,
// @ts-ignore
- soomSetWall: setWall,
+ soomWall: soomWallState,
+ // @ts-ignore
+ dashboard: dashboardState,
+ // @ts-ignore
+ brand: brandState,
+ // @ts-ignore
+ user: userState,
},
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware({
+ serializableCheck: false,
+ }),
});
export default store;
diff --git a/www/frontend/helpar/src/store/reducers/brand.tsx b/www/frontend/helpar/src/store/reducers/brand.tsx
new file mode 100644
index 0000000..a3880d5
--- /dev/null
+++ b/www/frontend/helpar/src/store/reducers/brand.tsx
@@ -0,0 +1,54 @@
+import { CachedBrand, CachedHQ } from "../../services/Session";
+import FAQ from "../../models/brands/FAQ";
+
+interface BrandState {
+ brand: Nullable;
+ faqs: FAQ[];
+ hq: Nullable;
+}
+type State = BrandState;
+
+export enum ActionType {
+ SET_BRAND = "brand:set_brand",
+ SET_FAQS = "brand:set_faqs",
+ SET_HQ = "brand:set_hq",
+}
+
+const initialState: State = {
+ brand: CachedBrand.load(),
+ faqs: [],
+ hq: CachedHQ.load(),
+};
+
+export interface SetBrandAction {
+ type: ActionType.SET_BRAND;
+ value: CachedBrand;
+}
+
+export interface SetFAQsAction {
+ type: ActionType.SET_FAQS;
+ value: FAQ[];
+}
+
+export interface SetHQAction {
+ type: ActionType.SET_HQ;
+ value: CachedHQ;
+}
+
+export type Action = SetBrandAction | SetFAQsAction | SetHQAction;
+
+export const brandState = (
+ state: State = initialState,
+ action: Action,
+): State => {
+ switch (action.type) {
+ case ActionType.SET_BRAND:
+ return { ...state, brand: action.value };
+ case ActionType.SET_FAQS:
+ return { ...state, faqs: action.value };
+ case ActionType.SET_HQ:
+ return { ...state, hq: action.value };
+ default:
+ return state;
+ }
+};
diff --git a/www/frontend/helpar/src/store/reducers/dashboard.tsx b/www/frontend/helpar/src/store/reducers/dashboard.tsx
new file mode 100644
index 0000000..385323d
--- /dev/null
+++ b/www/frontend/helpar/src/store/reducers/dashboard.tsx
@@ -0,0 +1,83 @@
+interface DashboardState {
+ loaded: boolean;
+ num_customers: number;
+ preferences_recorded: number;
+ frustrations_resolved: number;
+ reorder_revenue: number;
+ customer_data: {
+ nps: number;
+ customer_types: {
+ [key: string]: number;
+ };
+ registrations: [
+ {
+ user_name: string;
+ user_identifier: string;
+ },
+ ];
+ };
+ setup_process: {
+ num_happy_setups: number;
+ num_sad_setups: number;
+ num_neutral_setups: number;
+ setup_process_breakdown: {
+ [key: string]: number;
+ };
+ };
+ reorder_revenue_breakdown: {
+ [key: string]: number;
+ };
+ support_topics: {
+ [key: string]: number;
+ };
+ social_clicks: {
+ [key: string]: string;
+ };
+}
+
+type State = DashboardState;
+
+export enum ActionType {
+ SET_DATA = "dashboard:set_dashboard_data",
+}
+
+const initialState: State = {
+ loaded: false,
+ num_customers: 0,
+ preferences_recorded: 0,
+ frustrations_resolved: 0,
+ reorder_revenue: 0,
+ customer_data: {
+ nps: 0,
+ customer_types: {},
+ registrations: [{ user_name: "", user_identifier: "" }],
+ },
+ setup_process: {
+ num_happy_setups: 0,
+ num_sad_setups: 0,
+ num_neutral_setups: 0,
+ setup_process_breakdown: {},
+ },
+ reorder_revenue_breakdown: {},
+ support_topics: {},
+ social_clicks: {},
+};
+
+export interface SetDataAction {
+ type: ActionType.SET_DATA;
+ value: State;
+}
+
+export type Action = SetDataAction;
+
+export const dashboardState = (
+ state: State = initialState,
+ action: Action,
+): State => {
+ switch (action.type) {
+ case ActionType.SET_DATA:
+ return { ...action.value, loaded: true };
+ default:
+ return state;
+ }
+};
diff --git a/www/frontend/helpar/src/store/reducers/soom.tsx b/www/frontend/helpar/src/store/reducers/soom.tsx
index 02d2429..ad77ccd 100644
--- a/www/frontend/helpar/src/store/reducers/soom.tsx
+++ b/www/frontend/helpar/src/store/reducers/soom.tsx
@@ -1,9 +1,9 @@
type State = Array;
export enum ActionType {
- HAND = "hand",
- WALL = "wall",
- NULL = "null",
+ HAND = "soom:hand",
+ WALL = "soom:wall",
+ NULL = "soom:null",
}
const initialState: State = [];
@@ -20,7 +20,10 @@ export interface WallAction {
export type Action = HandAction | WallAction;
-export const setHand = (state: State = initialState, action: Action): State => {
+export const soomHandState = (
+ state: State = initialState,
+ action: Action,
+): State => {
switch (action.type) {
case ActionType.HAND:
return [...state, action.value];
@@ -29,7 +32,10 @@ export const setHand = (state: State = initialState, action: Action): State => {
}
};
-export const setWall = (state: State = initialState, action: Action): State => {
+export const soomWallState = (
+ state: State = initialState,
+ action: Action,
+): State => {
switch (action.type) {
case ActionType.WALL:
return [...state, action.value];
diff --git a/www/frontend/helpar/src/store/reducers/user.tsx b/www/frontend/helpar/src/store/reducers/user.tsx
new file mode 100644
index 0000000..4885dec
--- /dev/null
+++ b/www/frontend/helpar/src/store/reducers/user.tsx
@@ -0,0 +1,32 @@
+import { Session } from "../../services/Session";
+
+interface UserState {
+ session: Nullable;
+}
+
+type State = UserState;
+
+export enum ActionType {
+ SET_SESSION = "user:set_session",
+}
+
+const initialState: UserState = { session: Session.load() };
+
+export interface SetUserAction {
+ type: ActionType.SET_SESSION;
+ value: Session;
+}
+
+export type Action = SetUserAction;
+
+export const userState = (
+ state: State = initialState,
+ action: Action,
+): State => {
+ switch (action.type) {
+ case ActionType.SET_SESSION:
+ return { ...state, session: action.value };
+ default:
+ return state;
+ }
+};
diff --git a/www/frontend/helpar/src/utils.tsx b/www/frontend/helpar/src/utils.tsx
index 72515c0..54d82b8 100644
--- a/www/frontend/helpar/src/utils.tsx
+++ b/www/frontend/helpar/src/utils.tsx
@@ -10,7 +10,9 @@ export const isMobileDevice = (): boolean => {
export const alertMessages = (alert: Nullable): Nullable => {
switch (alert) {
case "device":
- return "Please only access HelpAR via a mobile device.";
+ return "Please access this experience via a mobile device.";
+ case "device-d":
+ return "Please access this experience via a desktop device.";
case "brand":
return "Doesn't look like you're on a branded experience.";
case "phone":
@@ -75,3 +77,30 @@ export const deriveCachedBrandName = (): string => {
return brand;
};
+
+export const dashboardStartDate = (): Date => {
+ const today = new Date();
+ return new Date(today.getFullYear(), today.getMonth(), 1);
+};
+
+export const dashboardEndDate = (): Date => {
+ return new Date();
+};
+
+export const simpleDateFormat = (date: Date): string => {
+ const day = date.getDate().toString().padStart(2, "0");
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
+ const year = date.getFullYear();
+
+ return `${month}-${day}-${year}`;
+};
+
+export const shortFormat = (value: number): string => {
+ if (value < 1000) {
+ return value.toString();
+ } else if (value < 1000000) {
+ return (value / 1000).toFixed(1) + "K";
+ } else {
+ return (value / 1000000).toFixed(1) + "M";
+ }
+};
diff --git a/www/frontend/helpar/src/views/HQ.tsx b/www/frontend/helpar/src/views/HQ.tsx
deleted file mode 100644
index 9f08a2c..0000000
--- a/www/frontend/helpar/src/views/HQ.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import React, { useEffect, useRef } from "react";
-import { useNavigate, useLocation } from "react-router-dom";
-import HQ from "../models/brands/HQ";
-import Brand from "../models/brands/Brand";
-import TileGrid from "../components/TileGrid";
-import { CardDescription, CardHeader, CardTitle } from "../components/ui/card";
-import axios from "../services/Axios";
-import { Session, CachedBrand, CachedHQ } from "../services/Session";
-import { isMobileDevice, deriveCachedBrandName } from "../utils";
-
-const BrandHQ = (): React.JSX.Element => {
- const navigate = useNavigate();
- const location = useLocation();
- const [loading, setLoading] = React.useState(false);
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [error, setError] = React.useState(null);
- const [session, setSession] = React.useState(Session.load());
- const [hqSession, setHQSession] = React.useState(CachedHQ.load());
- const cachedBrand = useRef(CachedBrand.load());
- const canonicalName = deriveCachedBrandName();
- const query = new URLSearchParams(location.search);
- const pkce = query.get("code");
-
- useEffect(() => {
- if (!isMobileDevice()) {
- return navigate("/?alert=device");
- }
- }, [navigate]);
-
- useEffect(() => {
- const fetchBrandInfo = async (canonicalName: string) => {
- try {
- setLoading(true);
- const response = await axios.get(
- `/brands/canonical-brand-info/${canonicalName}/`,
- );
- if (response.status === 200 && response.data && response.data.data) {
- const brand = Brand.fromJSON(response.data.data.brand);
- const bsesh = new CachedBrand(brand);
- bsesh.save();
- cachedBrand.current = bsesh;
- console.log("Successfully fetched brand info.");
- setLoading(false);
- } else {
- console.error("Failed to fetch brand:", response);
- setError("Failed to fetch brand.");
- setLoading(false);
- }
- } catch (error: any) {
- console.error("Failed to fetch brand:", error.toString());
- setError(error.toString());
- setLoading(false);
- }
- };
-
- if (!cachedBrand.current.isValid()) {
- console.log("Brand session not valid. Fetching a new one.");
- fetchBrandInfo(canonicalName);
- }
- }, [cachedBrand, canonicalName]);
-
- useEffect(() => {
- const authenticateViaPkce = async () => {
- try {
- const response = await axios.post(`/users/auth/pkce/`, {
- code: pkce,
- });
-
- if (response.status === 200 && response.data && response.data.success) {
- const sesh = Session.fromUser(response.data.user);
- setSession(sesh);
- console.log("Successfully authenticated via pkce.");
- } else {
- console.error("Failed to authenticate:", response);
- return navigate("/?alert=expired");
- }
- } catch (error: any) {
- console.error("Validation failed:", error.toString());
- return navigate("/?alert=expired");
- }
- };
-
- if (!session.isValid()) {
- console.log("User session not valid.");
- if (pkce) {
- console.log("Pkce found. Will authenticate via pkce");
- authenticateViaPkce();
- } else {
- console.log("Pkce not found. Redirecting home.");
- return navigate("/?alert=expired");
- }
- }
- }, [pkce, cachedBrand, session, navigate, canonicalName]);
-
- useEffect(() => {
- const fetchHQInfo = async () => {
- try {
- setLoading(true);
- const response = await axios.get(
- `/brands/${cachedBrand.current.brand()?.id}/hq/`,
- {
- headers: { Authorization: `Bearer ${session.jwt()}` },
- },
- );
- if (response.status === 200 && response.data && response.data.data) {
- const hq = HQ.fromJSON(response.data.data);
- const hqsesh = new CachedHQ(hq, cachedBrand.current.brand());
- hqsesh.save();
- setHQSession(hqsesh);
- console.log("Successfully fetched HQ info.");
- }
- setLoading(false);
- } catch (err: any) {
- console.error(`Could not fetch HQ info:`, err.message);
- setError(err.message);
- setLoading(false);
- }
- };
-
- console.log(
- session.isValid(),
- cachedBrand.current.isValid(),
- hqSession.isValid(),
- );
-
- if (
- session.isValid() &&
- cachedBrand.current.isValid() &&
- !hqSession.isValid()
- ) {
- console.log(
- "User and brand session valid. HQ is invalid. Fetching HQ info.",
- );
- fetchHQInfo();
- }
- }, [session, cachedBrand, hqSession]);
-
- if (loading || !hqSession.isValid()) {
- return (
-
-
-
-
{" "}
-
-
Loading...
-
-
- );
- } else {
- return (
-
-
-
navigate("/hq")}
- alt=""
- />
-
-
-
-
- Welcome to Soom
-
-
- How can we help with your Soom today?
-
-
-
-
-
-
-
{
- navigate(hqSession.hq().metadata.about);
- }}
- >
- More about Soom
-
-
-
-
-
- );
- }
-};
-
-export default BrandHQ;
diff --git a/www/frontend/helpar/src/views/Index.tsx b/www/frontend/helpar/src/views/Index.tsx
new file mode 100644
index 0000000..39c5048
--- /dev/null
+++ b/www/frontend/helpar/src/views/Index.tsx
@@ -0,0 +1,18 @@
+import React, { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { isMobileDevice } from "../utils";
+
+const Index = (): React.JSX.Element => {
+ const navigate = useNavigate();
+ useEffect(() => {
+ if (!isMobileDevice()) {
+ return navigate("/d");
+ }
+
+ return navigate("/m");
+ }, [navigate]);
+
+ return <>>;
+};
+
+export default Index;
diff --git a/www/frontend/helpar/src/views/enterprise/Account.tsx b/www/frontend/helpar/src/views/enterprise/Account.tsx
new file mode 100644
index 0000000..1ea39d8
--- /dev/null
+++ b/www/frontend/helpar/src/views/enterprise/Account.tsx
@@ -0,0 +1,20 @@
+import { Separator } from "./../../components/ui/separator";
+import { AccountForm } from "./../../components/forms/AccountForm";
+
+export default function SettingsAccount() {
+ return (
+
+
+
+
Account
+
+ Update your account settings. Set your preferred language and
+ timezone.
+
+
+
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/views/enterprise/Authenticate.tsx b/www/frontend/helpar/src/views/enterprise/Authenticate.tsx
new file mode 100644
index 0000000..7da07bc
--- /dev/null
+++ b/www/frontend/helpar/src/views/enterprise/Authenticate.tsx
@@ -0,0 +1,113 @@
+import React, { useEffect } from "react";
+import { useNavigate, Link } from "react-router-dom";
+import AuthenticationForm from "../../components/AuthenticationForm";
+import { deriveCachedBrandName, isMobileDevice } from "../../utils";
+
+const Authenticate = (): React.JSX.Element => {
+ const navigate = useNavigate();
+ const canonical = deriveCachedBrandName();
+ useEffect(() => {
+ if (isMobileDevice()) {
+ return navigate("/d?alert=device-d");
+ }
+ }, [navigate]);
+
+ return (
+
+
+
+
+
+
+
+ Login
+
+
+
+
+
+
navigate("/d")}>
+ {canonical[0].toUpperCase() + canonical.slice(1)}
+
+
+
+
+
+ “This library has saved me countless hours of work and
+ helped me deliver stunning designs to my clients faster than
+ ever before.”
+
+
+
+
+
+
+
+
+
+ Sign in
+
+
+ Sign in to your HelpAR account
+
+
+
+
+ By clicking continue, you agree to our{" "}
+
+ Terms of Service
+ {" "}
+ and{" "}
+
+ Privacy Policy
+
+ .
+
+
+
+
+
+ );
+};
+
+export default Authenticate;
diff --git a/www/frontend/helpar/src/views/enterprise/Billing.tsx b/www/frontend/helpar/src/views/enterprise/Billing.tsx
new file mode 100644
index 0000000..1d01285
--- /dev/null
+++ b/www/frontend/helpar/src/views/enterprise/Billing.tsx
@@ -0,0 +1,14 @@
+import { Separator } from "./../../components/ui/separator";
+
+export default function SettingsBilling() {
+ return (
+
+ );
+}
diff --git a/www/frontend/helpar/src/views/enterprise/Dashboard.tsx b/www/frontend/helpar/src/views/enterprise/Dashboard.tsx
new file mode 100644
index 0000000..721e17c
--- /dev/null
+++ b/www/frontend/helpar/src/views/enterprise/Dashboard.tsx
@@ -0,0 +1,348 @@
+import React, { useEffect } from "react";
+import { useNavigate, useLocation } from "react-router-dom";
+import { useSelector } from "react-redux";
+import { Card, CardContent, CardHeader } from "../../components/ui/card";
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "../../components/ui/tabs";
+import { Button } from "../../components/ui/button";
+import TopNavigation from "../../components/dashboard/TopNavigation";
+import { CalendarDateRangePicker } from "../../components/dashboard/DateRangePicker";
+import { SocialMedia } from "../../components/dashboard/SocialMedia";
+import { SupportTopics } from "../../components/dashboard/SupportTopics";
+import { Summary } from "../../components/dashboard/Summary";
+import { CustomerType } from "../../components/dashboard/CustomerType";
+import { RecentCustomers } from "../../components/dashboard/RecentCustomers";
+import { SessionLocation } from "../../components/dashboard/SessionLocation";
+import {
+ useAuthenticationService,
+ useBrandService,
+ useDashboardService,
+} from "../../services";
+import {
+ isMobileDevice,
+ dashboardStartDate,
+ dashboardEndDate,
+} from "../../utils";
+
+const Dashboard = (): React.JSX.Element => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const query = new URLSearchParams(location.search);
+ const pkce = query.get("code");
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const { pkceAuth } = useAuthenticationService();
+ const { fetchBrandInfo } = useBrandService();
+ const { fetchDashboardData } = useDashboardService();
+ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+ const [error, setError] = React.useState(null);
+ const dashboard = useSelector((state: any) => state.dashboard);
+ const [startDate, setStartDate] = React.useState(dashboardStartDate());
+ const [endDate, setEndDate] = React.useState(dashboardEndDate());
+
+ useEffect(() => {
+ if (isMobileDevice()) {
+ return navigate("/d?alert=device-d");
+ }
+ }, [navigate]);
+
+ useEffect(() => {
+ if (!session.isValid()) {
+ console.log("User session invalid.");
+ if (pkce) {
+ console.log("PKCE code found. Will authenticate via PKCE.");
+ pkceAuth(pkce, true);
+ } else {
+ console.log("PKCE code not found. Redirecting home.");
+ return navigate("/d?alert=auth");
+ }
+ }
+ }, [pkce, session, pkceAuth, navigate]);
+
+ useEffect(() => {
+ if (!brand.isValid()) {
+ console.log("Brand session invalid.");
+ fetchBrandInfo();
+ }
+ }, [fetchBrandInfo, brand]);
+
+ useEffect(() => {
+ if (session.isValid() && brand.isValid()) {
+ console.log(`Fetching dashboard data.`);
+ fetchDashboardData(startDate, endDate, setError);
+ } else {
+ console.log(
+ `${session.isValid()} ${brand.isValid()} - Skipping dashboard data fetch.`,
+ );
+ }
+ }, [session, startDate, brand, fetchDashboardData, endDate]);
+
+ const onCalendarChange = (start: Date, end: Date) => {
+ setStartDate(start);
+ setEndDate(end);
+ };
+
+ return dashboard.loading ? (
+
+
Loading
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
Dashboard
+
+
+
+
+
+
+
+ Overview
+
+ Analytics
+
+
+ Reports
+
+
+ Notifications
+
+
+
+
+
+
+ Sessions
+
+
+
+ 45,231
+
+ +20.1% from last month
+
+
+
+
+
+ Users
+
+
+
+ 2,350
+
+ +180.1% from last month
+
+
+
+
+
+ Reorders
+
+
+
+ $12,234
+
+ +19% from last month
+
+
+
+
+
+ Preferences Recorded
+
+
+
+ 573
+
+ -10% from last month
+
+
+
+
+
+
+
+ Summary
+
+
+
+
+
+
+
+
+
+ Customer Type
+
+
+
+
+
+
+
+
+
+ Setup Process
+
+
+
+
+
+
+
+
+
+
+
+ Support Topics
+
+
+
+
+
+
+
+
+
+ Reorder Revenue
+
+
+
+
+
+
+
+
+
+
+
+ Recent Customers
+
+
+
+
+
+
+
+
+
+ Session Location
+
+
+
+
+
+
+
+
+
+ Social Media
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Created with ❤️ by HelpAR
+
+
+ );
+};
+
+export default Dashboard;
diff --git a/www/frontend/helpar/src/views/enterprise/Index.tsx b/www/frontend/helpar/src/views/enterprise/Index.tsx
new file mode 100644
index 0000000..f50a649
--- /dev/null
+++ b/www/frontend/helpar/src/views/enterprise/Index.tsx
@@ -0,0 +1,74 @@
+import React, { useEffect } from "react";
+import { useNavigate, useLocation } from "react-router-dom";
+import { useSelector } from "react-redux";
+import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert";
+import { Button } from "../../components/ui/button";
+import { useBrandService } from "../../services";
+import {
+ isMobileDevice,
+ deriveCachedBrandName,
+ alertMessages,
+} from "../../utils";
+
+const EnterpriseHome = (): React.JSX.Element => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const brand = useSelector((state: any) => state.brand.brand);
+ const canonical = deriveCachedBrandName();
+ const [alertModal, setAlertModal] = React.useState(
+ null,
+ );
+ const queryParams = new URLSearchParams(location.search);
+ const alertParam = queryParams.get("alert");
+ const { fetchBrandInfo } = useBrandService();
+
+ useEffect(() => {
+ if (isMobileDevice()) {
+ return navigate("/d?alert=device-d");
+ }
+ }, [navigate]);
+
+ useEffect(() => {
+ if (!brand.isValid()) {
+ fetchBrandInfo();
+ }
+ }, [canonical, brand, fetchBrandInfo, navigate]);
+
+ useEffect(() => {
+ const message = alertMessages(alertParam);
+ if (message) {
+ setAlertModal(
+
+ Heads up!
+
+
+ {message}
+
+
+ ,
+ );
+ }
+ }, [alertParam, setAlertModal]);
+
+ return (
+
+
Enterprise Home
+
+ {alertModal}
+
+ );
+};
+
+export default EnterpriseHome;
diff --git a/www/frontend/helpar/src/views/enterprise/Profile.tsx b/www/frontend/helpar/src/views/enterprise/Profile.tsx
new file mode 100644
index 0000000..b5020a6
--- /dev/null
+++ b/www/frontend/helpar/src/views/enterprise/Profile.tsx
@@ -0,0 +1,19 @@
+import { Separator } from "./../../components/ui/separator";
+import { EnterpriseProfileForm } from "./../../components/forms/EnterpriseProfileForm";
+
+export default function SettingsProfile() {
+ return (
+
+
+
+
Profile
+
+ This is how others will see you on the site.
+
+
+
+
+
+
+ );
+}
diff --git a/www/frontend/helpar/src/views/enterprise/Settings.tsx b/www/frontend/helpar/src/views/enterprise/Settings.tsx
new file mode 100644
index 0000000..1f2f5dc
--- /dev/null
+++ b/www/frontend/helpar/src/views/enterprise/Settings.tsx
@@ -0,0 +1,72 @@
+import React, { useState } from "react";
+import Profile from ".//Profile";
+import Account from ".//Account";
+import Billing from ".//Billing";
+import TopNavigation from "../../components/dashboard/TopNavigation";
+import SidebarNav, { NavbarItem } from "../../components/dashboard/SidebarNav";
+
+const sidebarNavItems = [
+ {
+ title: "Profile",
+ },
+ {
+ title: "Account",
+ },
+ {
+ title: "Billing",
+ },
+];
+
+const Settings = (): React.JSX.Element => {
+ const [currNavbarItem, setCurrNavbarItem] = useState("Profile");
+
+ const render = () => {
+ switch (currNavbarItem) {
+ case "Profile":
+ return ;
+ case "Account":
+ return ;
+ case "Billing":
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/www/frontend/helpar/src/views/About.tsx b/www/frontend/helpar/src/views/user/About.tsx
similarity index 66%
rename from www/frontend/helpar/src/views/About.tsx
rename to www/frontend/helpar/src/views/user/About.tsx
index e3677c1..10e7bb5 100644
--- a/www/frontend/helpar/src/views/About.tsx
+++ b/www/frontend/helpar/src/views/user/About.tsx
@@ -1,9 +1,9 @@
import React, { useEffect, useRef } from "react";
import { useLocation, useNavigate } from "react-router-dom";
-import { Session, CachedBrand } from "../services/Session";
-import Events from "../services/Events";
-import config, { Environment } from "../config";
-import { waitFor, isMobileDevice } from "../utils";
+import { useSelector } from "react-redux";
+import Events from "../../services/Events";
+import config, { Environment } from "../../config";
+import { waitFor, isMobileDevice } from "../../utils";
const About = (): React.JSX.Element => {
const location = useLocation();
@@ -11,29 +11,29 @@ const About = (): React.JSX.Element => {
const query = new URLSearchParams(location.search);
const uri = query.get("r");
const domain = uri?.split("/")[2];
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
- if (!cachedBrand.current.isValid()) {
- return navigate("/?alert=brand");
+ if (!brand.isValid()) {
+ return navigate("/m?alert=brand");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, brand, navigate]);
useEffect(() => {
const sendEvents = async () => {
await events.current.sendUserInteraction({
module: "about",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "redirect",
diff --git a/www/frontend/helpar/src/views/Authenticate.tsx b/www/frontend/helpar/src/views/user/Authenticate.tsx
similarity index 92%
rename from www/frontend/helpar/src/views/Authenticate.tsx
rename to www/frontend/helpar/src/views/user/Authenticate.tsx
index f0dc52c..478b038 100644
--- a/www/frontend/helpar/src/views/Authenticate.tsx
+++ b/www/frontend/helpar/src/views/user/Authenticate.tsx
@@ -2,11 +2,12 @@ import React, { useEffect, useRef } from "react";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { useNavigate, useLocation } from "react-router-dom";
+import { useSelector } from "react-redux";
import { LogoGoogle, LogoApple, Call } from "react-ionicons";
import validator from "validator";
import uuid from "react-uuid";
import { zodResolver } from "@hookform/resolvers/zod";
-import { Button } from "./../components/ui/button";
+import { Button } from "../../components/ui/button";
import {
Form,
FormControl,
@@ -14,20 +15,19 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from "./../components/ui/form";
-import { Input } from "./../components/ui/input";
-import { CachedBrand, Session } from "./../services/Session";
-import axios from "./../services/Axios";
-import Events from "./../services/Events";
-import { DEFAULT_CONTRY_CODE } from "../constants";
+} from "../../components/ui/form";
+import { Input } from "../../components/ui/input";
+import axios from "../../services/Axios";
+import Events from "../../services/Events";
+import { DEFAULT_CONTRY_CODE } from "../../constants";
import {
isMobileDevice,
normalizePhoneNumber,
sanitizeError,
-} from "./../utils";
-import config, { Environment, Module } from "./../config";
+} from "../../utils";
+import config, { Environment, Module } from "../../config";
-import "./../index.css";
+import "./../../index.css";
interface AuthenticationFormProps {
redirect: Nullable;
@@ -73,7 +73,7 @@ const AuthenticationForm = (props: AuthenticationFormProps) => {
if (response.status === 200 && response.data && response.data.success) {
setLoading(false);
- navigate(`/confirm-code?p=${phone}`);
+ navigate(`/m/confirm-code?p=${phone}`);
} else {
console.error("Failed to authenticate phone number:", response);
props.handlePhoneError("Failed to authenticate phone number.");
@@ -162,10 +162,9 @@ const AuthenticateView = (): React.JSX.Element => {
const state = useRef(uuid());
const [loading, setLoading] = React.useState(false);
const [oauthError, setOauthError] = React.useState("");
-
const [phoneError, setPhoneError] = React.useState("");
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
+ const session = useSelector((state: any) => state.session);
+ const brand = useSelector((state: any) => state.brand);
const queryParams = new URLSearchParams(location.search);
const redirect = queryParams.get("redirect");
@@ -191,13 +190,13 @@ const AuthenticateView = (): React.JSX.Element => {
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (cachedBrand.current.isValid() && session.current.isValid()) {
- return navigate("/hq");
+ if (!!brand && !!session) {
+ return navigate("/m/hq");
}
- }, [cachedBrand, navigate, session]);
+ }, [navigate, session, brand]);
const oauthGoogleRequest = async () => {
try {
diff --git a/www/frontend/helpar/src/views/Chat.tsx b/www/frontend/helpar/src/views/user/Chat.tsx
similarity index 85%
rename from www/frontend/helpar/src/views/Chat.tsx
rename to www/frontend/helpar/src/views/user/Chat.tsx
index 92beb5c..eefc330 100644
--- a/www/frontend/helpar/src/views/Chat.tsx
+++ b/www/frontend/helpar/src/views/user/Chat.tsx
@@ -2,13 +2,14 @@ import React, { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { SendOutline } from "react-ionicons";
import uuid from "react-uuid";
+import { useSelector, useDispatch } from "react-redux";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { useForm } from "react-hook-form";
-import { ScrollArea } from "../components/ui/scroll-area";
-import { Textarea } from "../components/ui/textarea";
-import { ChatTopNavigation } from "./../components/TopNavigation";
-import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert";
+import { ScrollArea } from "../../components/ui/scroll-area";
+import { Textarea } from "../../components/ui/textarea";
+import { ChatTopNavigation } from "../../components/TopNavigation";
+import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert";
import {
Form,
FormControl,
@@ -16,16 +17,17 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from "./../components/ui/form";
-import { Button } from "../components/ui/button";
-import { Input } from "./../components/ui/input";
-import { Session, CachedBrand } from "../services/Session";
-import Events from "../services/Events";
-import axios from "../services/Axios";
-import Message from "../models/brands/Message";
-import { isMobileDevice, sanitizeError } from "../utils";
-import config, { Environment, Module } from "../config";
-import { LLM_ACTOR } from "../constants";
+} from "../../components/ui/form";
+import { Button } from "../../components/ui/button";
+import { Input } from "../../components/ui/input";
+import { Session } from "../../services/Session";
+import Events from "../../services/Events";
+import axios from "../../services/Axios";
+import Message from "../../models/brands/Message";
+import { ActionType } from "../../store/reducers/user";
+import { isMobileDevice, sanitizeError } from "../../utils";
+import config, { Environment, Module } from "../../config";
+import { LLM_ACTOR } from "../../constants";
const makeGreeting = (name: string): string => {
const greetings = [
@@ -66,12 +68,13 @@ const ChatItem = (props: ChatItemProps): React.JSX.Element => {
const Chat = (): React.JSX.Element => {
const navigate = useNavigate();
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
+ const dispatch = useDispatch();
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
const [alertModal, setAlertModal] =
React.useState();
const [usernameError, setUsernameError] = React.useState("");
- const events = useRef(new Events(session.current));
+ const events = useRef(new Events(session));
const [threadId, setThreadId] = React.useState>(null);
const [sending, setSending] = React.useState(false);
const [messages, setMessages] = React.useState([]);
@@ -83,17 +86,17 @@ const Chat = (): React.JSX.Element => {
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
- if (!cachedBrand.current.isValid()) {
- return navigate("/?alert=brand");
+ if (!brand.isValid()) {
+ return navigate("/m?alert=brand");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, brand, navigate]);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
const sendMessage = async (message: string) => {
@@ -108,7 +111,7 @@ const Chat = (): React.JSX.Element => {
await events.current.sendUserInteraction({
module: "chat",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -137,12 +140,11 @@ const Chat = (): React.JSX.Element => {
}
const response = await axios.post(
- `/brands/${cachedBrand.current.brand()
- ?.id}/threads/${localThreadId}/messages/`,
+ `/brands/${brand.brand?.id}/threads/${localThreadId}/messages/`,
{ content: localMessage.content },
{
headers: {
- Authorization: `Bearer ${session.current.jwt()}`,
+ Authorization: `Bearer ${session.jwt()}`,
"Content-Type": "application/json",
},
},
@@ -197,7 +199,7 @@ const Chat = (): React.JSX.Element => {
},
{
headers: {
- Authorization: `Bearer ${session.current.jwt()}`,
+ Authorization: `Bearer ${session.jwt()}`,
"Content-Type": "application/json",
},
},
@@ -206,7 +208,10 @@ const Chat = (): React.JSX.Element => {
if (response.status === 200 && response.data && response.data.success) {
const sesh = Session.fromUser(response.data.user);
sesh.save();
- session.current = sesh;
+ dispatch({
+ type: ActionType.SET_SESSION,
+ value: sesh,
+ });
sendMessage(makeGreeting(values.username));
setAlertModal(null);
setUsernameSet(true);
@@ -221,7 +226,7 @@ const Chat = (): React.JSX.Element => {
};
useEffect(() => {
- const name = session.current.username();
+ const name = session.firstName();
if (name && !usernameSet) {
setUsernameSet(true);
sendMessage(makeGreeting(name));
@@ -229,7 +234,7 @@ const Chat = (): React.JSX.Element => {
}, [session, usernameSet, sendMessage]);
useEffect(() => {
- const name = session.current.username();
+ const name = session.firstName();
if (!name && !usernameSet)
setAlertModal(
@@ -300,7 +305,7 @@ const Chat = (): React.JSX.Element => {
const sendMetric = async (ellapsed: number) => {
await events.current.sendUserTimeInteraction({
module: Module.Support,
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
timing: ellapsed,
metadata: {
@@ -319,7 +324,7 @@ const Chat = (): React.JSX.Element => {
setTimingSent(true);
};
- }, [startTime, timingSent]);
+ }, [startTime, brand.brand?.id, timingSent]);
const handleTextareaChange = (e: any) => {
setMessage(e.target.value);
@@ -337,8 +342,8 @@ const Chat = (): React.JSX.Element => {
} absolute z-10 bg-black opacity-50 w-full h-full`}
>
{messages.map((message: any, i: number) => (
diff --git a/www/frontend/helpar/src/views/ConfirmCode.tsx b/www/frontend/helpar/src/views/user/ConfirmCode.tsx
similarity index 85%
rename from www/frontend/helpar/src/views/ConfirmCode.tsx
rename to www/frontend/helpar/src/views/user/ConfirmCode.tsx
index 9349d4a..7431ff7 100644
--- a/www/frontend/helpar/src/views/ConfirmCode.tsx
+++ b/www/frontend/helpar/src/views/user/ConfirmCode.tsx
@@ -1,9 +1,10 @@
import React, { useEffect, useRef } from "react";
import * as z from "zod";
import { useForm } from "react-hook-form";
+import { useDispatch } from "react-redux";
import { useNavigate, useLocation } from "react-router-dom";
import { zodResolver } from "@hookform/resolvers/zod";
-import { Button } from "./../components/ui/button";
+import { Button } from "../../components/ui/button";
import {
Form,
FormControl,
@@ -11,14 +12,16 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from "./../components/ui/form";
-import { Input } from "./../components/ui/input";
-import axios from "./../services/Axios";
-import { Session } from "../services/Session";
-import Events from "./../services/Events";
-import { DEFAULT_CONTRY_CODE } from "../constants";
-import { isMobileDevice } from "./../utils";
-import "./../index.css";
+} from "../../components/ui/form";
+import { Input } from "../../components/ui/input";
+import axios from "../../services/Axios";
+import { Session } from "../../services/Session";
+import Events from "../../services/Events";
+import { ActionType } from "../../store/reducers/user";
+import { DEFAULT_CONTRY_CODE } from "../../constants";
+import { isMobileDevice } from "../../utils";
+
+import "./../../index.css";
interface ConfirmCodeFormProps {
phone: string;
@@ -27,7 +30,7 @@ interface ConfirmCodeFormProps {
const ConfirmCodeForm = (props: ConfirmCodeFormProps) => {
const navigate = useNavigate();
- const sessionRef = useRef(Session.load());
+ const dispatch = useDispatch();
const [loading, setLoading] = React.useState(false);
const [codeError, setCodeError] = React.useState("");
@@ -65,8 +68,10 @@ const ConfirmCodeForm = (props: ConfirmCodeFormProps) => {
if (response.status === 200 && response.data && response.data.success) {
setLoading(false);
- sessionRef.current = Session.fromUser(response.data.user);
- navigate(`/hq`);
+ const session = Session.fromUser(response.data.user);
+ session.save();
+ dispatch({ type: ActionType.SET_SESSION, value: session });
+ navigate(`/m/hq`);
} else {
console.error("Failed to confirm phone number:", response);
setCodeError("Failed to confirm phone number.");
@@ -141,11 +146,11 @@ const ConfirmCodeView = (): React.JSX.Element => {
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
if (!phone) {
- return navigate("/?alert=phone");
+ return navigate("/m?alert=phone");
}
}, [navigate, phone]);
diff --git a/www/frontend/helpar/src/views/CreateAccount.tsx b/www/frontend/helpar/src/views/user/CreateAccount.tsx
similarity index 93%
rename from www/frontend/helpar/src/views/CreateAccount.tsx
rename to www/frontend/helpar/src/views/user/CreateAccount.tsx
index 3e0d061..00d9ee2 100644
--- a/www/frontend/helpar/src/views/CreateAccount.tsx
+++ b/www/frontend/helpar/src/views/user/CreateAccount.tsx
@@ -3,7 +3,7 @@ import * as z from "zod";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { zodResolver } from "@hookform/resolvers/zod";
-import { Button } from "../components/ui/button";
+import { Button } from "../../components/ui/button";
import {
Form,
FormControl,
@@ -11,9 +11,9 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from "../components/ui/form";
-import { Input } from "../components/ui/input";
-import { isMobileDevice } from "../utils";
+} from "../../components/ui/form";
+import { Input } from "../../components/ui/input";
+import { isMobileDevice } from "../../utils";
const formSchema = z.object({
email: z.string().email({
@@ -115,7 +115,7 @@ const CreateAccountForm = () => {
className="bg-white border border-black text-black w-50 mt-10"
type="submit"
size="lg"
- onClick={() => navigate("/soom/hq")}
+ onClick={() => navigate("/m/soom/hq")}
>
Update Account
@@ -128,7 +128,7 @@ const CreateAccountView = (): React.JSX.Element => {
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
});
diff --git a/www/frontend/helpar/src/views/user/HQ.tsx b/www/frontend/helpar/src/views/user/HQ.tsx
new file mode 100644
index 0000000..9b5589f
--- /dev/null
+++ b/www/frontend/helpar/src/views/user/HQ.tsx
@@ -0,0 +1,127 @@
+import React, { useEffect } from "react";
+import { useNavigate, useLocation } from "react-router-dom";
+import { GlobeOutline } from "react-ionicons";
+import { useSelector } from "react-redux";
+import TileGrid from "../../components/TileGrid";
+import {
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../components/ui/card";
+import { useAuthenticationService, useBrandService } from "../../services";
+import { isMobileDevice } from "../../utils";
+
+const BrandHQ = (): React.JSX.Element => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [loading, setLoading] = React.useState(false);
+ const query = new URLSearchParams(location.search);
+ const pkce = query.get("code");
+ const brand = useSelector((state: any) => state.brand.brand);
+ const hq = useSelector((state: any) => state.brand.hq);
+ const session = useSelector((state: any) => state.user.session);
+ const { pkceAuth } = useAuthenticationService();
+ const { fetchBrandInfo, fetchHQInfo } = useBrandService();
+
+ useEffect(() => {
+ if (!isMobileDevice()) {
+ return navigate("/m?alert=device");
+ }
+ }, [navigate]);
+
+ useEffect(() => {
+ if (!brand.isValid()) {
+ console.log("Brand not found. Will fetch brand info.");
+ fetchBrandInfo();
+ }
+ }, [brand, fetchBrandInfo]);
+
+ useEffect(() => {
+ if (!session.isValid()) {
+ console.log("User session invalid.");
+ if (pkce) {
+ console.log("PKCE code found. Will authenticate via PKCE.");
+ pkceAuth(pkce, false);
+ } else {
+ console.log("PKCE code not found. Redirecting home.");
+ return navigate("/m?alert=auth");
+ }
+ }
+ }, [pkce, session, pkceAuth, navigate]);
+
+ useEffect(() => {
+ console.log(session.isValid(), brand.isValid(), hq.isValid());
+
+ if (session.isValid() && brand.isValid() && !hq.isValid()) {
+ setLoading(true);
+ console.log(
+ "User and brand session valid. HQ is invalid. Fetching HQ info.",
+ );
+ fetchHQInfo();
+
+ setLoading(false);
+ }
+ }, [session, brand, hq, fetchHQInfo]);
+
+ if (loading || !hq.isValid()) {
+ return (
+
+
+
+
{" "}
+
+
Loading...
+
+
+ );
+ } else {
+ return (
+
+
+
navigate("/m/hq")}
+ alt=""
+ />
+
+
+
+
+ Welcome to Soom
+
+
+ How can we help with your Soom today?
+
+
+
+
+
+
+ {
+ navigate(hq.hq.metadata.about);
+ }}
+ style={{ color: "white", height: 25, width: 25 }}
+ />
+
+
+
+
+ );
+ }
+};
+
+export default BrandHQ;
diff --git a/www/frontend/helpar/src/views/Home.tsx b/www/frontend/helpar/src/views/user/Index.tsx
similarity index 59%
rename from www/frontend/helpar/src/views/Home.tsx
rename to www/frontend/helpar/src/views/user/Index.tsx
index 5971827..a30ec16 100644
--- a/www/frontend/helpar/src/views/Home.tsx
+++ b/www/frontend/helpar/src/views/user/Index.tsx
@@ -1,72 +1,42 @@
import React, { useEffect, useRef } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { ChevronUpIcon } from "@radix-ui/react-icons";
-import axios from "../services/Axios";
-import Events from "../services/Events";
-import { CachedBrand, Session } from "../services/Session";
-import Brand from "../models/brands/Brand";
-import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert";
-import { Button } from "../components/ui/button";
-import { alertMessages, deriveCachedBrandName, isMobileDevice } from "../utils";
-import config, { Environment, Module } from "../config";
+import { useSelector } from "react-redux";
+import Events from "../../services/Events";
+import { useBrandService } from "../../services";
+import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert";
+import { Button } from "../../components/ui/button";
+import { alertMessages, isMobileDevice } from "../../utils";
+import config, { Environment, Module } from "../../config";
const HomeView = (): React.JSX.Element => {
const location = useLocation();
const navigate = useNavigate();
- const canonicalName = deriveCachedBrandName();
+ const brand = useSelector((state: any) => state.brand.brand);
+ const session = useSelector((state: any) => state.user.session);
const [alertModal, setAlertModal] = React.useState(
null,
);
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [error, setError] = React.useState(null);
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [loading, setLoading] = React.useState(false);
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const events = useRef(new Events(session));
const queryParams = new URLSearchParams(location.search);
const alertParam = queryParams.get("alert");
+ const { fetchBrandInfo } = useBrandService();
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (session.current.isValid()) {
- return navigate("/hq");
+ if (session.isValid()) {
+ return navigate("/m/hq");
}
}, [session, navigate]);
useEffect(() => {
- const fetchBrandInfo = async (canonicalName: string) => {
- try {
- setLoading(true);
- const response = await axios.get(
- `/brands/canonical-brand-info/${canonicalName}/`,
- );
- if (response.status === 200 && response.data && response.data.data) {
- const brand = Brand.fromJSON(response.data.data.brand);
- const bsesh = new CachedBrand(brand);
- bsesh.save();
- cachedBrand.current = bsesh;
- console.log("Successfully fetched brand info.");
- setLoading(false);
- } else {
- console.error("Failed to fetch brand:", response);
- setError("Failed to fetch brand.");
- setLoading(false);
- }
- } catch (error: any) {
- console.error("Failed to fetch brand:", error.toString());
- setError(error.toString());
- setLoading(false);
- }
- };
-
- if (!cachedBrand.current.isValid()) {
- fetchBrandInfo(canonicalName);
+ if (!brand.isValid()) {
+ fetchBrandInfo();
}
- }, [canonicalName, cachedBrand, navigate]);
+ }, [brand, fetchBrandInfo]);
useEffect(() => {
const message = alertMessages(alertParam);
@@ -120,8 +90,8 @@ const HomeView = (): React.JSX.Element => {
},
});
- if (cachedBrand.current.isValid() && session.current.isValid()) {
- return navigate("/hq");
+ if (!!brand && !!session) {
+ return navigate("/m/hq");
}
}}
alt="soom-logo"
@@ -137,7 +107,7 @@ const HomeView = (): React.JSX.Element => {
action: "to_authenticate",
},
});
- navigate("/authenticate");
+ navigate("/m/authenticate");
}}
>
diff --git a/www/frontend/helpar/src/views/Support.tsx b/www/frontend/helpar/src/views/user/Support.tsx
similarity index 82%
rename from www/frontend/helpar/src/views/Support.tsx
rename to www/frontend/helpar/src/views/user/Support.tsx
index 850ea49..85a2cb9 100644
--- a/www/frontend/helpar/src/views/Support.tsx
+++ b/www/frontend/helpar/src/views/user/Support.tsx
@@ -1,16 +1,16 @@
import React, { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
+import { useSelector } from "react-redux";
import { ChevronDown, ChevronUp, ChatbubbleOutline } from "react-ionicons";
-import { FAQTopNavigation } from "../components/TopNavigation";
-import Searchbar from "../components/Searchbar";
-import { Button } from "../components/ui/button";
-import { ScrollArea } from "../components/ui/scroll-area";
-import { CachedBrand, Session } from "../services/Session";
-import FAQ from "../models/brands/FAQ";
-import axios from "../services/Axios";
-import Events from "../services/Events";
-import { isMobileDevice } from "../utils";
-import config, { Environment, Module } from "../config";
+import { FAQTopNavigation } from "../../components/TopNavigation";
+import Searchbar from "../../components/Searchbar";
+import { Button } from "../../components/ui/button";
+import { ScrollArea } from "../../components/ui/scroll-area";
+import FAQ from "../../models/brands/FAQ";
+import axios from "../../services/Axios";
+import Events from "../../services/Events";
+import { isMobileDevice } from "../../utils";
+import config, { Environment, Module } from "../../config";
interface FAQItemProps {
question: string;
@@ -69,9 +69,9 @@ const FAQs = (): React.JSX.Element => {
const [error, setError] = React.useState
(null);
const [originalFAQs, setOriginalFAQs] = React.useState([]);
const [faqs, setFAQs] = React.useState([]);
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
const [query, setQuery] = React.useState("");
const [startTime, setStartTime] = React.useState(Date.now());
const [timingSent, setTimingSent] = React.useState(false);
@@ -82,7 +82,7 @@ const FAQs = (): React.JSX.Element => {
const sendMetric = async (ellapsed: number) => {
await events.current.sendUserTimeInteraction({
module: Module.Support,
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
timing: ellapsed,
metadata: {
@@ -101,7 +101,7 @@ const FAQs = (): React.JSX.Element => {
setTimingSent(true);
};
- }, [startTime, timingSent]);
+ }, [startTime, brand.brand?.id, timingSent]);
const onSearch = (value: string) => {
if (!value) {
@@ -121,11 +121,11 @@ const FAQs = (): React.JSX.Element => {
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
}, [navigate, session]);
@@ -134,12 +134,9 @@ const FAQs = (): React.JSX.Element => {
try {
setLoading(true);
- const response = await axios.get(
- `/brands/${cachedBrand.current.brand()?.id}/faqs/`,
- {
- headers: { Authorization: `Bearer ${session.current.jwt()}` },
- },
- );
+ const response = await axios.get(`/brands/${brand.brand?.id}/faqs/`, {
+ headers: { Authorization: `Bearer ${session.jwt()}` },
+ });
if (response.status === 200 && response.data && response.data.data) {
const data = response.data.data.map((item: any, i: number) =>
FAQ.fromJSON(item),
@@ -161,7 +158,7 @@ const FAQs = (): React.JSX.Element => {
fetchFAQs();
setLoaded(true);
}
- }, [session, cachedBrand, loaded]);
+ }, [session, brand, loaded]);
return (
@@ -181,7 +178,7 @@ const FAQs = (): React.JSX.Element => {
navigate("/hq")}
+ onClick={() => navigate("/m/hq")}
alt=""
/>
@@ -194,14 +191,14 @@ const FAQs = (): React.JSX.Element => {
onClick={async () => {
await events.current.sendUserInteraction({
module: "faq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "nav-to",
component: "chat",
},
});
- navigate("/chat");
+ navigate("/m/chat");
}}
>
{
const navigate = useNavigate();
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
const [startTime, setStartTime] = React.useState(Date.now());
const [timingSent, setTimingSent] = React.useState(false);
@@ -26,7 +26,7 @@ const GettingStarted = (): React.JSX.Element => {
const sendMetric = async (ellapsed: number) => {
await events.current.sendUserTimeInteraction({
module: Module.GettingStarted,
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
timing: ellapsed,
metadata: {
@@ -45,17 +45,17 @@ const GettingStarted = (): React.JSX.Element => {
setTimingSent(true);
};
- }, [startTime, timingSent]);
+ }, [startTime, brand.brand?.id, timingSent]);
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, navigate]);
return (
{
navigate("/hq")}
+ onClick={() => navigate("/m/hq")}
alt=""
/>
navigate("/hq")}
+ onClick={() => navigate("/m/hq")}
alt=""
/>
- Install Your Soom
+ Install Your Soom
Follow these steps to set up your Soom Shower
@@ -107,14 +107,14 @@ const GettingStarted = (): React.JSX.Element => {
onClick={async () => {
await events.current.sendUserInteraction({
module: "install",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
component: "wallmount-showerhead",
},
});
- navigate("/soom/wall-hq");
+ navigate("/m/soom/wall-hq");
}}
/>
@@ -138,7 +138,7 @@ const GettingStarted = (): React.JSX.Element => {
onClick={async () => {
await events.current.sendUserInteraction({
module: "install",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -146,7 +146,7 @@ const GettingStarted = (): React.JSX.Element => {
},
});
- navigate("/soom/hand-hq");
+ navigate("/m/soom/hand-hq");
}}
/>
diff --git a/www/frontend/helpar/src/views/soom/WallHQ.tsx b/www/frontend/helpar/src/views/user/soom/WallHQ.tsx
similarity index 78%
rename from www/frontend/helpar/src/views/soom/WallHQ.tsx
rename to www/frontend/helpar/src/views/user/soom/WallHQ.tsx
index fa6f84f..76cbdb2 100644
--- a/www/frontend/helpar/src/views/soom/WallHQ.tsx
+++ b/www/frontend/helpar/src/views/user/soom/WallHQ.tsx
@@ -1,22 +1,17 @@
import React, { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
-import { useSelector, useDispatch } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import Slider from "react-slick";
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import { configureStore } from "@reduxjs/toolkit";
-import { Session, CachedBrand } from "../../services/Session";
-import Events from "../../services/Events";
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import { setWall, ActionType } from "../../store/reducers/soom";
-import { Button } from "../../components/ui/button";
+import Events from "../../../services/Events";
+import { ActionType } from "../../../store/reducers/soom";
+import { Button } from "../../../components/ui/button";
import {
CardDescription,
CardHeader,
CardTitle,
-} from "../../components/ui/card";
-import config, { Environment } from "../../config";
-import { isMobileDevice } from "../../utils";
+} from "../../../components/ui/card";
+import config, { Environment } from "../../../config";
+import { isMobileDevice } from "../../../utils";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
@@ -24,24 +19,24 @@ import "slick-carousel/slick/slick-theme.css";
const WallHQ = (): React.JSX.Element => {
const navigate = useNavigate();
const dispatch = useDispatch();
- const session = useRef
(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
- const wall = useSelector((state: any) => state.soomSetWall);
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
+ const wall = useSelector((state: any) => state.soomWall);
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
- if (!cachedBrand.current.isValid()) {
- return navigate("/?alert=brand");
+ if (!brand.isValid()) {
+ return navigate("/m?alert=brand");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, brand, navigate]);
const toPage = (url: string, n: number): void => {
dispatch({ type: ActionType.WALL, value: n });
@@ -70,14 +65,14 @@ const WallHQ = (): React.JSX.Element => {
navigate("/soom/getting-started")}
+ onClick={() => navigate("/m/soom/getting-started")}
alt=""
/>
navigate("/hq")}
+ onClick={() => navigate("/m/hq")}
alt=""
/>
@@ -107,14 +102,14 @@ const WallHQ = (): React.JSX.Element => {
onClick={async () => {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
component: "wall-filter",
},
});
- toPage("/soom/wall-filter", 1);
+ toPage("/m/soom/wall-filter", 1);
}}
>
{
onClick={async () => {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
component: "wall-pods",
},
});
- toPage("/soom/wall-pods", 2);
+ toPage("/m/soom/wall-pods", 2);
}}
>
{
onClick={async () => {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
component: "wall-mount",
},
});
- toPage("/soom/wall-mount", 3);
+ toPage("/m/soom/wall-mount", 3);
}}
>
{
const style: React.CSSProperties = {
@@ -24,9 +24,9 @@ const HandFilter: React.FC = () => {
height: "100%",
};
const navigate = useNavigate();
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
const [swiperPagination, setSwiperPagination] = useState(0);
const swiperRef = useRef(null);
const [isLastSlide, setIsLastSlide] = useState(false);
@@ -40,7 +40,7 @@ const HandFilter: React.FC = () => {
const sendMetric = async (ellapsed: number) => {
await events.current.sendUserTimeInteraction({
module: Module.GettingStarted,
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
timing: ellapsed,
metadata: {
@@ -59,7 +59,7 @@ const HandFilter: React.FC = () => {
);
setTimingSent(true);
};
- }, [startTime, timingSent]);
+ }, [startTime, brand, timingSent]);
useEffect(() => {
swiper();
@@ -67,13 +67,13 @@ const HandFilter: React.FC = () => {
useEffect(() => {
if (!isMobileDevice()) {
- navigate("/?alert=device");
+ navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- navigate("/?alert=expired");
+ if (!session.isValid()) {
+ navigate("/m?alert=expired");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, navigate]);
const swiper = () => {
swiperRef.current = new Swiper(".swiper", {
@@ -93,7 +93,7 @@ const HandFilter: React.FC = () => {
if (isLastSlide) {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -101,11 +101,11 @@ const HandFilter: React.FC = () => {
details: "finished",
},
});
- navigate("/soom/wall-hq");
+ navigate("/m/soom/wall-hq");
} else {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -132,7 +132,7 @@ const HandFilter: React.FC = () => {
/>
-
-
-
);
};
diff --git a/www/frontend/helpar/src/views/soom/hand/HandMount.tsx b/www/frontend/helpar/src/views/user/soom/hand/HandMount.tsx
similarity index 85%
rename from www/frontend/helpar/src/views/soom/hand/HandMount.tsx
rename to www/frontend/helpar/src/views/user/soom/hand/HandMount.tsx
index fdbbd99..a0dbbba 100644
--- a/www/frontend/helpar/src/views/soom/hand/HandMount.tsx
+++ b/www/frontend/helpar/src/views/user/soom/hand/HandMount.tsx
@@ -1,15 +1,14 @@
import React, { useEffect, useState, useRef } from "react";
-/* eslint-disable no-undef */
+import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { ChevronRightIcon } from "@radix-ui/react-icons";
import Swiper from "swiper";
-import { Button } from "../../../components/ui/button";
-import { Session, CachedBrand } from "../../../services/Session";
-import Events from "../../../services/Events";
-import config, { Environment, Module } from "../../../config";
-import { isMobileDevice } from "../../../utils";
+import { Button } from "../../../../components/ui/button";
+import Events from "../../../../services/Events";
+import config, { Environment, Module } from "../../../../config";
+import { isMobileDevice } from "../../../../utils";
-import "../../../styles/styles.css";
+import "../../../../styles/styles.css";
const HandMount: React.FC = () => {
const style: React.CSSProperties = {
@@ -24,9 +23,9 @@ const HandMount: React.FC = () => {
height: "100%",
};
const navigate = useNavigate();
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
const [swiperPagination, setSwiperPagination] = useState(0);
const swiperRef = useRef(null);
const [isLastSlide, setIsLastSlide] = useState(false);
@@ -39,7 +38,7 @@ const HandMount: React.FC = () => {
const sendMetric = async (ellapsed: number) => {
await events.current.sendUserTimeInteraction({
module: Module.GettingStarted,
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
timing: ellapsed,
metadata: {
@@ -58,17 +57,17 @@ const HandMount: React.FC = () => {
);
setTimingSent(true);
};
- }, [startTime, timingSent]);
+ }, [startTime, brand, timingSent]);
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, navigate]);
useEffect(() => {
swiper();
@@ -90,11 +89,11 @@ const HandMount: React.FC = () => {
const handleButtonClick = async () => {
if (isLastSlide) {
- navigate("/soom/wall-hq");
+ navigate("/m/soom/wall-hq");
await events.current.sendUserInteraction({
module: "hand-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -105,7 +104,7 @@ const HandMount: React.FC = () => {
} else {
await events.current.sendUserInteraction({
module: "hand-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -128,7 +127,7 @@ const HandMount: React.FC = () => {
/>
- navigate("/support")} className="z-50">
+ navigate("/m/support")} className="z-50">
{
-
-
);
};
diff --git a/www/frontend/helpar/src/views/soom/hand/HandPods.tsx b/www/frontend/helpar/src/views/user/soom/hand/HandPods.tsx
similarity index 87%
rename from www/frontend/helpar/src/views/soom/hand/HandPods.tsx
rename to www/frontend/helpar/src/views/user/soom/hand/HandPods.tsx
index 4f80873..51a1426 100644
--- a/www/frontend/helpar/src/views/soom/hand/HandPods.tsx
+++ b/www/frontend/helpar/src/views/user/soom/hand/HandPods.tsx
@@ -1,15 +1,15 @@
import React, { useEffect, useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronRightIcon } from "@radix-ui/react-icons";
+import { useSelector } from "react-redux";
import Swiper from "swiper";
-import { Button } from "../../../components/ui/button";
-import PopupCard from "../../../components/PopupCard";
-import { Session, CachedBrand } from "../../../services/Session";
-import Events from "../../../services/Events";
-import { isMobileDevice } from "../../../utils";
-import config, { Environment, Module } from "../../../config";
+import { Button } from "../../../../components/ui/button";
+import PopupCard from "../../../../components/PopupCard";
+import Events from "../../../../services/Events";
+import { isMobileDevice } from "../../../../utils";
+import config, { Environment, Module } from "../../../../config";
-import "../../../styles/styles.css";
+import "../../../../styles/styles.css";
const HandPods: React.FC = () => {
const style: React.CSSProperties = {
@@ -24,9 +24,9 @@ const HandPods: React.FC = () => {
height: "100%",
};
const navigate = useNavigate();
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
const [swiperPagination, setSwiperPagination] = useState(0);
const swiperRef = useRef(null);
const [isLastSlide, setIsLastSlide] = useState(false);
@@ -40,13 +40,13 @@ const HandPods: React.FC = () => {
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, navigate]);
useEffect(() => {
setStartTime(Date.now());
@@ -54,7 +54,7 @@ const HandPods: React.FC = () => {
const sendMetric = async (ellapsed: number) => {
await events.current.sendUserTimeInteraction({
module: Module.GettingStarted,
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
timing: ellapsed,
metadata: {
@@ -73,7 +73,7 @@ const HandPods: React.FC = () => {
);
setTimingSent(true);
};
- }, [startTime, timingSent]);
+ }, [startTime, brand, timingSent]);
const swiper = () => {
swiperRef.current = new Swiper(".swiper", {
@@ -93,7 +93,7 @@ const HandPods: React.FC = () => {
if (isLastSlide) {
await events.current.sendUserInteraction({
module: "hand-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -101,11 +101,11 @@ const HandPods: React.FC = () => {
details: "finished",
},
});
- navigate("/soom/wall-hq");
+ navigate("/m/soom/wall-hq");
} else {
await events.current.sendUserInteraction({
module: "hand-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -132,7 +132,7 @@ const HandPods: React.FC = () => {
/>
- navigate("/support")} className="z-50">
+ navigate("/m/support")} className="z-50">
{
-
-
);
};
diff --git a/www/frontend/helpar/src/views/soom/wall/WallFilter.tsx b/www/frontend/helpar/src/views/user/soom/wall/WallFilter.tsx
similarity index 87%
rename from www/frontend/helpar/src/views/soom/wall/WallFilter.tsx
rename to www/frontend/helpar/src/views/user/soom/wall/WallFilter.tsx
index bbd708a..7da1f77 100644
--- a/www/frontend/helpar/src/views/soom/wall/WallFilter.tsx
+++ b/www/frontend/helpar/src/views/user/soom/wall/WallFilter.tsx
@@ -1,16 +1,15 @@
import React, { useEffect, useState, useRef } from "react";
-/* eslint-disable no-undef */
import { useNavigate } from "react-router-dom";
import Swiper from "swiper";
import { ChevronRightIcon } from "@radix-ui/react-icons";
-import { Button } from "../../../components/ui/button";
-import PopupCard from "../../../components/PopupCard";
-import { Session, CachedBrand } from "../../../services/Session";
-import Events from "../../../services/Events";
-import { isMobileDevice } from "../../../utils";
-import config, { Environment, Module } from "../../../config";
+import { useSelector } from "react-redux";
+import { Button } from "../../../../components/ui/button";
+import PopupCard from "../../../../components/PopupCard";
+import Events from "../../../../services/Events";
+import { isMobileDevice } from "../../../../utils";
+import config, { Environment, Module } from "../../../../config";
-import "../../../styles/styles.css";
+import "../../../../styles/styles.css";
const WallFilter: React.FC = () => {
const style: React.CSSProperties = {
@@ -25,9 +24,9 @@ const WallFilter: React.FC = () => {
height: "100%",
};
const navigate = useNavigate();
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
const [swiperPagination, setSwiperPagination] = useState(0);
const swiperRef = useRef(null);
const [isLastSlide, setIsLastSlide] = useState(false);
@@ -41,7 +40,7 @@ const WallFilter: React.FC = () => {
const sendMetric = async (ellapsed: number) => {
await events.current.sendUserTimeInteraction({
module: Module.GettingStarted,
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
timing: ellapsed,
metadata: {
@@ -60,7 +59,7 @@ const WallFilter: React.FC = () => {
);
setTimingSent(true);
};
- }, [startTime, timingSent]);
+ }, [startTime, brand.brand?.id, timingSent]);
useEffect(() => {
swiper();
@@ -68,15 +67,15 @@ const WallFilter: React.FC = () => {
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
}, [navigate]);
useEffect(() => {
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, navigate]);
const swiper = () => {
swiperRef.current = new Swiper(".swiper", {
@@ -96,7 +95,7 @@ const WallFilter: React.FC = () => {
if (isLastSlide) {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -105,11 +104,11 @@ const WallFilter: React.FC = () => {
},
});
- navigate("/soom/wall-hq");
+ navigate("/m/soom/wall-hq");
} else {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -137,7 +136,7 @@ const WallFilter: React.FC = () => {
/>
- navigate("/support")} className="z-50">
+ navigate("/m/support")} className="z-50">
{
-
);
};
diff --git a/www/frontend/helpar/src/views/soom/wall/WallMount.tsx b/www/frontend/helpar/src/views/user/soom/wall/WallMount.tsx
similarity index 87%
rename from www/frontend/helpar/src/views/soom/wall/WallMount.tsx
rename to www/frontend/helpar/src/views/user/soom/wall/WallMount.tsx
index 1c1ae49..df7bc5a 100644
--- a/www/frontend/helpar/src/views/soom/wall/WallMount.tsx
+++ b/www/frontend/helpar/src/views/user/soom/wall/WallMount.tsx
@@ -1,14 +1,14 @@
import React, { useEffect, useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronRightIcon } from "@radix-ui/react-icons";
+import { useSelector } from "react-redux";
import Swiper from "swiper";
-import { Button } from "../../../components/ui/button";
-import Events from "../../../services/Events";
-import { Session, CachedBrand } from "../../../services/Session";
-import { isMobileDevice } from "../../../utils";
-import config, { Environment, Module } from "../../../config";
+import { Button } from "../../../../components/ui/button";
+import Events from "../../../../services/Events";
+import { isMobileDevice } from "../../../../utils";
+import config, { Environment, Module } from "../../../../config";
-import "../../../styles/styles.css";
+import "../../../../styles/styles.css";
const WallMount = (): React.JSX.Element => {
const style: React.CSSProperties = {
@@ -23,9 +23,9 @@ const WallMount = (): React.JSX.Element => {
height: "100%",
};
const navigate = useNavigate();
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
const [swiperPagination, setSwiperPagination] = useState(0);
const swiperRef = useRef(null);
const [isLastSlide, setIsLastSlide] = useState(false);
@@ -38,7 +38,7 @@ const WallMount = (): React.JSX.Element => {
const sendMetric = async (ellapsed: number) => {
await events.current.sendUserTimeInteraction({
module: Module.GettingStarted,
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
timing: ellapsed,
metadata: {
@@ -57,7 +57,7 @@ const WallMount = (): React.JSX.Element => {
);
setTimingSent(true);
};
- }, [startTime, timingSent]);
+ }, [startTime, brand.brand?.id, timingSent]);
useEffect(() => {
swiper();
@@ -65,13 +65,13 @@ const WallMount = (): React.JSX.Element => {
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, navigate]);
const swiper = () => {
swiperRef.current = new Swiper(".swiper", {
@@ -91,7 +91,7 @@ const WallMount = (): React.JSX.Element => {
if (isLastSlide) {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -99,11 +99,11 @@ const WallMount = (): React.JSX.Element => {
details: "finished",
},
});
- navigate("/soom/wall-hq");
+ navigate("/m/soom/wall-hq");
} else {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -126,7 +126,7 @@ const WallMount = (): React.JSX.Element => {
/>
- navigate("/support")} className="z-50">
+ navigate("/m/support")} className="z-50">
{
-
-
);
};
diff --git a/www/frontend/helpar/src/views/soom/wall/WallPods.tsx b/www/frontend/helpar/src/views/user/soom/wall/WallPods.tsx
similarity index 87%
rename from www/frontend/helpar/src/views/soom/wall/WallPods.tsx
rename to www/frontend/helpar/src/views/user/soom/wall/WallPods.tsx
index c5c8979..dc49119 100644
--- a/www/frontend/helpar/src/views/soom/wall/WallPods.tsx
+++ b/www/frontend/helpar/src/views/user/soom/wall/WallPods.tsx
@@ -1,15 +1,15 @@
import React, { useEffect, useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronRightIcon } from "@radix-ui/react-icons";
+import { useSelector } from "react-redux";
import Swiper from "swiper";
-import { Button } from "../../../components/ui/button";
-import PopupCard from "../../../components/PopupCard";
-import { Session, CachedBrand } from "../../../services/Session";
-import Events from "../../../services/Events";
-import { isMobileDevice } from "../../../utils";
-import config, { Environment, Module } from "../../../config";
+import { Button } from "../../../../components/ui/button";
+import PopupCard from "../../../../components/PopupCard";
+import Events from "../../../../services/Events";
+import { isMobileDevice } from "../../../../utils";
+import config, { Environment, Module } from "../../../../config";
-import "../../../styles/styles.css";
+import "../../../../styles/styles.css";
const WallPods: React.FC = () => {
const style: React.CSSProperties = {
@@ -24,9 +24,9 @@ const WallPods: React.FC = () => {
height: "100%",
};
const navigate = useNavigate();
- const session = useRef(Session.load());
- const cachedBrand = useRef(CachedBrand.load());
- const events = useRef(new Events(session.current));
+ const session = useSelector((state: any) => state.user.session);
+ const brand = useSelector((state: any) => state.brand.brand);
+ const events = useRef(new Events(session));
const [swiperPagination, setSwiperPagination] = useState(0);
const swiperRef = useRef(null);
const [isLastSlide, setIsLastSlide] = useState(false);
@@ -44,7 +44,7 @@ const WallPods: React.FC = () => {
const sendMetric = async (ellapsed: number) => {
await events.current.sendUserTimeInteraction({
module: Module.GettingStarted,
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
timing: ellapsed,
metadata: {
@@ -64,17 +64,17 @@ const WallPods: React.FC = () => {
setTimingSent(true);
};
- }, [startTime, timingSent]);
+ }, [startTime, brand.brand?.id, timingSent]);
useEffect(() => {
if (!isMobileDevice()) {
- return navigate("/?alert=device");
+ return navigate("/m?alert=device");
}
- if (!session.current.isValid()) {
- return navigate("/?alert=expired");
+ if (!session.isValid()) {
+ return navigate("/m?alert=expired");
}
- }, [session, cachedBrand, navigate]);
+ }, [session, navigate]);
const swiper = () => {
swiperRef.current = new Swiper(".swiper", {
@@ -94,7 +94,7 @@ const WallPods: React.FC = () => {
if (isLastSlide) {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -102,11 +102,11 @@ const WallPods: React.FC = () => {
details: "finished",
},
});
- navigate("/soom/wall-hq");
+ navigate("/m/soom/wall-hq");
} else {
await events.current.sendUserInteraction({
module: "wall-hq",
- brand_id: cachedBrand.current.brand()?.id,
+ brand_id: brand.brand?.id,
test: config.environment === Environment.Development,
metadata: {
type: "click",
@@ -132,7 +132,7 @@ const WallPods: React.FC = () => {
/>
- navigate("/support")} className="z-50">
+ navigate("/m/support")} className="z-50">
{
-
);
};
diff --git a/www/frontend/helpar/yarn.lock b/www/frontend/helpar/yarn.lock
index d6d87ec..f1166d2 100644
--- a/www/frontend/helpar/yarn.lock
+++ b/www/frontend/helpar/yarn.lock
@@ -48,19 +48,19 @@
integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==
"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
- version "7.23.6"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.6.tgz#8be77cd77c55baadcc1eae1c33df90ab6d2151d4"
- integrity sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.7.tgz#4d8016e06a14b5f92530a13ed0561730b5c6483f"
+ integrity sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.23.5"
"@babel/generator" "^7.23.6"
"@babel/helper-compilation-targets" "^7.23.6"
"@babel/helper-module-transforms" "^7.23.3"
- "@babel/helpers" "^7.23.6"
+ "@babel/helpers" "^7.23.7"
"@babel/parser" "^7.23.6"
"@babel/template" "^7.22.15"
- "@babel/traverse" "^7.23.6"
+ "@babel/traverse" "^7.23.7"
"@babel/types" "^7.23.6"
convert-source-map "^2.0.0"
debug "^4.1.0"
@@ -112,10 +112,10 @@
lru-cache "^5.1.1"
semver "^6.3.1"
-"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.22.15", "@babel/helper-create-class-features-plugin@^7.23.6":
- version "7.23.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.6.tgz#b04d915ce92ce363666f816a884cdcfc9be04953"
- integrity sha512-cBXU1vZni/CpGF29iTu4YRbOZt3Wat6zCoMDxRF1MayiEc4URxOj31tT65HUM0CRpMowA3HCJaAOVOUnMf96cw==
+"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.22.15", "@babel/helper-create-class-features-plugin@^7.23.6", "@babel/helper-create-class-features-plugin@^7.23.7":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz#b2e6826e0e20d337143655198b79d58fdc9bd43d"
+ integrity sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
"@babel/helper-environment-visitor" "^7.22.20"
@@ -267,13 +267,13 @@
"@babel/template" "^7.22.15"
"@babel/types" "^7.22.19"
-"@babel/helpers@^7.23.6":
- version "7.23.6"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.6.tgz#d03af2ee5fb34691eec0cda90f5ecbb4d4da145a"
- integrity sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==
+"@babel/helpers@^7.23.7":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.7.tgz#eb543c36f81da2873e47b76ee032343ac83bba60"
+ integrity sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==
dependencies:
"@babel/template" "^7.22.15"
- "@babel/traverse" "^7.23.6"
+ "@babel/traverse" "^7.23.7"
"@babel/types" "^7.23.6"
"@babel/highlight@^7.23.4":
@@ -306,10 +306,10 @@
"@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
"@babel/plugin-transform-optional-chaining" "^7.23.3"
-"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.23.3":
- version "7.23.3"
- resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz#20c60d4639d18f7da8602548512e9d3a4c8d7098"
- integrity sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==
+"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.23.7":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz#516462a95d10a9618f197d39ad291a9b47ae1d7b"
+ integrity sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==
dependencies:
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-plugin-utils" "^7.22.5"
@@ -323,15 +323,12 @@
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-proposal-decorators@^7.16.4":
- version "7.23.6"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.6.tgz#b34e9837c4fb0277c6d571581c76595521cf2db4"
- integrity sha512-D7Ccq9LfkBFnow3azZGJvZYgcfeqAw3I1e5LoTpj6UKIFQilh8yqXsIGcRIqbBdsPWIz+Ze7ZZfggSj62Qp+Fg==
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.7.tgz#1d827902cbd3d9054e54fb2f2056cdd1eaa0e368"
+ integrity sha512-b1s5JyeMvqj7d9m9KhJNHKc18gEJiSyVzVX3bwbiPalQBQpuvfPh6lA9F7Kk/dWH0TIiXRpB9yicwijY6buPng==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.23.6"
+ "@babel/helper-create-class-features-plugin" "^7.23.7"
"@babel/helper-plugin-utils" "^7.22.5"
- "@babel/helper-replace-supers" "^7.22.20"
- "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
- "@babel/helper-split-export-declaration" "^7.22.6"
"@babel/plugin-syntax-decorators" "^7.23.3"
"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0":
@@ -551,10 +548,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-async-generator-functions@^7.23.4":
- version "7.23.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz#93ac8e3531f347fba519b4703f9ff2a75c6ae27a"
- integrity sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==
+"@babel/plugin-transform-async-generator-functions@^7.23.7":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz#3aa0b4f2fa3788b5226ef9346cf6d16ec61f99cd"
+ integrity sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==
dependencies:
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-plugin-utils" "^7.22.5"
@@ -915,15 +912,15 @@
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-transform-runtime@^7.16.4":
- version "7.23.6"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.6.tgz#bf853cd0a675c16ee33e6ba2a63b536e75e5d754"
- integrity sha512-kF1Zg62aPseQ11orDhFRw+aPG/eynNQtI+TyY+m33qJa2cJ5EEvza2P2BNTIA9E5MyqFABHEyY6CPHwgdy9aNg==
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz#52bbd20054855beb9deae3bee9ceb05289c343e6"
+ integrity sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw==
dependencies:
"@babel/helper-module-imports" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
- babel-plugin-polyfill-corejs2 "^0.4.6"
- babel-plugin-polyfill-corejs3 "^0.8.5"
- babel-plugin-polyfill-regenerator "^0.5.3"
+ babel-plugin-polyfill-corejs2 "^0.4.7"
+ babel-plugin-polyfill-corejs3 "^0.8.7"
+ babel-plugin-polyfill-regenerator "^0.5.4"
semver "^6.3.1"
"@babel/plugin-transform-shorthand-properties@^7.23.3":
@@ -1004,9 +1001,9 @@
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4":
- version "7.23.6"
- resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.6.tgz#ad0ea799d5a3c07db5b9a172819bbd444092187a"
- integrity sha512-2XPn/BqKkZCpzYhUUNZ1ssXw7DcXfKQEjv/uXZUXgaebCMYmkEsfZ2yY+vv+xtXv50WmL5SGhyB6/xsWxIvvOQ==
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.7.tgz#e5d69b9f14db8a13bae4d8e5ce7f360973626241"
+ integrity sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==
dependencies:
"@babel/compat-data" "^7.23.5"
"@babel/helper-compilation-targets" "^7.23.6"
@@ -1014,7 +1011,7 @@
"@babel/helper-validator-option" "^7.23.5"
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.23.3"
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.23.3"
- "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.23.3"
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.23.7"
"@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2"
"@babel/plugin-syntax-async-generators" "^7.8.4"
"@babel/plugin-syntax-class-properties" "^7.12.13"
@@ -1035,7 +1032,7 @@
"@babel/plugin-syntax-top-level-await" "^7.14.5"
"@babel/plugin-syntax-unicode-sets-regex" "^7.18.6"
"@babel/plugin-transform-arrow-functions" "^7.23.3"
- "@babel/plugin-transform-async-generator-functions" "^7.23.4"
+ "@babel/plugin-transform-async-generator-functions" "^7.23.7"
"@babel/plugin-transform-async-to-generator" "^7.23.3"
"@babel/plugin-transform-block-scoped-functions" "^7.23.3"
"@babel/plugin-transform-block-scoping" "^7.23.4"
@@ -1083,9 +1080,9 @@
"@babel/plugin-transform-unicode-regex" "^7.23.3"
"@babel/plugin-transform-unicode-sets-regex" "^7.23.3"
"@babel/preset-modules" "0.1.6-no-external-plugins"
- babel-plugin-polyfill-corejs2 "^0.4.6"
- babel-plugin-polyfill-corejs3 "^0.8.5"
- babel-plugin-polyfill-regenerator "^0.5.3"
+ babel-plugin-polyfill-corejs2 "^0.4.7"
+ babel-plugin-polyfill-corejs3 "^0.8.7"
+ babel-plugin-polyfill-regenerator "^0.5.4"
core-js-compat "^3.31.0"
semver "^6.3.1"
@@ -1126,10 +1123,10 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
-"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
- version "7.23.6"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d"
- integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.7.tgz#dd7c88deeb218a0f8bd34d5db1aa242e0f203193"
+ integrity sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==
dependencies:
regenerator-runtime "^0.14.0"
@@ -1142,10 +1139,10 @@
"@babel/parser" "^7.22.15"
"@babel/types" "^7.22.15"
-"@babel/traverse@^7.23.6", "@babel/traverse@^7.7.2":
- version "7.23.6"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.6.tgz#b53526a2367a0dd6edc423637f3d2d0f2521abc5"
- integrity sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==
+"@babel/traverse@^7.23.7", "@babel/traverse@^7.7.2":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305"
+ integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==
dependencies:
"@babel/code-frame" "^7.23.5"
"@babel/generator" "^7.23.6"
@@ -1173,9 +1170,9 @@
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@csstools/normalize.css@*":
- version "12.0.0"
- resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4"
- integrity sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==
+ version "12.1.1"
+ resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.1.1.tgz#f0ad221b7280f3fc814689786fd9ee092776ef8f"
+ integrity sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ==
"@csstools/postcss-cascade-layers@^1.1.1":
version "1.1.1"
@@ -1679,7 +1676,7 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
-"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
+"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.9":
version "0.3.20"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
@@ -1951,6 +1948,20 @@
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3"
+"@radix-ui/react-dropdown-menu@2.0.6":
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63"
+ integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==
+ dependencies:
+ "@babel/runtime" "^7.13.10"
+ "@radix-ui/primitive" "1.0.1"
+ "@radix-ui/react-compose-refs" "1.0.1"
+ "@radix-ui/react-context" "1.0.1"
+ "@radix-ui/react-id" "1.0.1"
+ "@radix-ui/react-menu" "2.0.6"
+ "@radix-ui/react-primitive" "1.0.3"
+ "@radix-ui/react-use-controllable-state" "1.0.1"
+
"@radix-ui/react-focus-guards@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
@@ -2093,6 +2104,28 @@
"@radix-ui/react-use-previous" "1.0.1"
"@radix-ui/react-visually-hidden" "1.0.3"
+"@radix-ui/react-popover@1.0.7":
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
+ integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==
+ dependencies:
+ "@babel/runtime" "^7.13.10"
+ "@radix-ui/primitive" "1.0.1"
+ "@radix-ui/react-compose-refs" "1.0.1"
+ "@radix-ui/react-context" "1.0.1"
+ "@radix-ui/react-dismissable-layer" "1.0.5"
+ "@radix-ui/react-focus-guards" "1.0.1"
+ "@radix-ui/react-focus-scope" "1.0.4"
+ "@radix-ui/react-id" "1.0.1"
+ "@radix-ui/react-popper" "1.1.3"
+ "@radix-ui/react-portal" "1.0.4"
+ "@radix-ui/react-presence" "1.0.1"
+ "@radix-ui/react-primitive" "1.0.3"
+ "@radix-ui/react-slot" "1.0.2"
+ "@radix-ui/react-use-controllable-state" "1.0.1"
+ aria-hidden "^1.1.1"
+ react-remove-scroll "2.5.5"
+
"@radix-ui/react-popper@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
@@ -2246,6 +2279,14 @@
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
+"@radix-ui/react-separator@1.0.3":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.0.3.tgz#be5a931a543d5726336b112f465f58585c04c8aa"
+ integrity sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==
+ dependencies:
+ "@babel/runtime" "^7.13.10"
+ "@radix-ui/react-primitive" "1.0.3"
+
"@radix-ui/react-slot@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.0.tgz#7fa805b99891dea1e862d8f8fbe07f4d6d0fd698"
@@ -2291,6 +2332,25 @@
"@radix-ui/react-roving-focus" "1.0.4"
"@radix-ui/react-use-controllable-state" "1.0.1"
+"@radix-ui/react-toast@1.1.5":
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6"
+ integrity sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==
+ dependencies:
+ "@babel/runtime" "^7.13.10"
+ "@radix-ui/primitive" "1.0.1"
+ "@radix-ui/react-collection" "1.0.3"
+ "@radix-ui/react-compose-refs" "1.0.1"
+ "@radix-ui/react-context" "1.0.1"
+ "@radix-ui/react-dismissable-layer" "1.0.5"
+ "@radix-ui/react-portal" "1.0.4"
+ "@radix-ui/react-presence" "1.0.1"
+ "@radix-ui/react-primitive" "1.0.3"
+ "@radix-ui/react-use-callback-ref" "1.0.1"
+ "@radix-ui/react-use-controllable-state" "1.0.1"
+ "@radix-ui/react-use-layout-effect" "1.0.1"
+ "@radix-ui/react-visually-hidden" "1.0.3"
+
"@radix-ui/react-toggle-group@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz#f5b5c8c477831b013bec3580c55e20a68179d6ec"
@@ -2413,6 +2473,11 @@
dependencies:
"@babel/runtime" "^7.13.10"
+"@react-leaflet/core@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-2.1.0.tgz#383acd31259d7c9ae8fb1b02d5e18fe613c2a13d"
+ integrity sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==
+
"@reduxjs/toolkit@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.0.1.tgz#0a5233c1e35c1941b03aece39cceade3467a1062"
@@ -2694,9 +2759,9 @@
"@babel/types" "^7.0.0"
"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6":
- version "7.20.4"
- resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.4.tgz#ec2c06fed6549df8bc0eb4615b683749a4a92e1b"
- integrity sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.5.tgz#7b7502be0aa80cc4ef22978846b983edaafcd4dd"
+ integrity sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==
dependencies:
"@babel/types" "^7.20.7"
@@ -2730,6 +2795,57 @@
dependencies:
"@types/node" "*"
+"@types/d3-array@^3.0.3":
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
+ integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
+
+"@types/d3-color@*":
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
+ integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-ease@^3.0.0":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
+ integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-interpolate@^3.0.1":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
+ integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-path@*":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.2.tgz#4327f4a05d475cf9be46a93fc2e0f8d23380805a"
+ integrity sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==
+
+"@types/d3-scale@^4.0.2":
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb"
+ integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==
+ dependencies:
+ "@types/d3-time" "*"
+
+"@types/d3-shape@^3.1.0":
+ version "3.1.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72"
+ integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==
+ dependencies:
+ "@types/d3-path" "*"
+
+"@types/d3-time@*", "@types/d3-time@^3.0.0":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be"
+ integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==
+
+"@types/d3-timer@^3.0.0":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
+ integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
"@types/eslint-scope@^3.7.3":
version "3.7.7"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5"
@@ -2776,6 +2892,11 @@
"@types/qs" "*"
"@types/serve-static" "*"
+"@types/geojson@*":
+ version "7946.0.13"
+ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.13.tgz#e6e77ea9ecf36564980a861e24e62a095988775e"
+ integrity sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==
+
"@types/graceful-fs@^4.1.2":
version "4.1.9"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4"
@@ -2842,6 +2963,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+"@types/leaflet@1.9.8":
+ version "1.9.8"
+ resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.8.tgz#32162a8eaf305c63267e99470b9603b5883e63e8"
+ integrity sha512-EXdsL4EhoUtGm2GC2ZYtXn+Fzc6pluVgagvo2VC1RHWToLGlTRwVYoDpqS/7QXa01rmDyBjJk3Catpf60VMkwg==
+ dependencies:
+ "@types/geojson" "*"
+
"@types/mime@*":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
@@ -2860,9 +2988,9 @@
"@types/node" "*"
"@types/node@*":
- version "20.10.5"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.5.tgz#47ad460b514096b7ed63a1dae26fad0914ed3ab2"
- integrity sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==
+ version "20.10.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5"
+ integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==
dependencies:
undici-types "~5.26.4"
@@ -2942,9 +3070,9 @@
"@types/react" "*"
"@types/react@*":
- version "18.2.45"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.45.tgz#253f4fac288e7e751ab3dc542000fb687422c15c"
- integrity sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==
+ version "18.2.46"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.46.tgz#f04d6c528f8f136ea66333bc66abcae46e2680df"
+ integrity sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -3339,9 +3467,14 @@ acorn@^7.1.1:
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.2.4, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0:
- version "8.11.2"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b"
- integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==
+ version "8.11.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
+ integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
+
+add@2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235"
+ integrity sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==
address@^1.0.1, address@^1.1.2:
version "1.2.2"
@@ -3728,7 +3861,7 @@ babel-plugin-named-asset-import@^0.3.8:
resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz#6b7fa43c59229685368683c28bc9734f24524cc2"
integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==
-babel-plugin-polyfill-corejs2@^0.4.6:
+babel-plugin-polyfill-corejs2@^0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz#679d1b94bf3360f7682e11f2cb2708828a24fe8c"
integrity sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==
@@ -3737,7 +3870,7 @@ babel-plugin-polyfill-corejs2@^0.4.6:
"@babel/helper-define-polyfill-provider" "^0.4.4"
semver "^6.3.1"
-babel-plugin-polyfill-corejs3@^0.8.5:
+babel-plugin-polyfill-corejs3@^0.8.7:
version "0.8.7"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz#941855aa7fdaac06ed24c730a93450d2b2b76d04"
integrity sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==
@@ -3745,7 +3878,7 @@ babel-plugin-polyfill-corejs3@^0.8.5:
"@babel/helper-define-polyfill-provider" "^0.4.4"
core-js-compat "^3.33.1"
-babel-plugin-polyfill-regenerator@^0.5.3:
+babel-plugin-polyfill-regenerator@^0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz#c6fc8eab610d3a11eb475391e52584bacfc020f4"
integrity sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==
@@ -3991,9 +4124,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001565:
- version "1.0.30001571"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz#4182e93d696ff42930f4af7eba515ddeb57917ac"
- integrity sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==
+ version "1.0.30001572"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz#1ccf7dc92d2ee2f92ed3a54e11b7b4a3041acfa0"
+ integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==
case-sensitive-paths-webpack-plugin@^2.4.0:
version "2.4.0"
@@ -4078,9 +4211,9 @@ class-variance-authority@0.7.0:
clsx "2.0.0"
classnames@^2.2.5:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.4.0.tgz#78011af2612f5c552195e2e657bd5fe2de973cac"
- integrity sha512-lWxiIlphgAhTLN657pwU/ofFxsUTOWc2CRIFeoV5st0MGRJHStUnWIUJgDHxjUO/F0mXzGufXIM4Lfu/8h+MpA==
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
+ integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
clean-css@^5.2.2:
version "5.3.3"
@@ -4103,6 +4236,11 @@ clsx@2.0.0:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
+clsx@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
+ integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
+
cmdk@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-0.2.0.tgz#53c52d56d8776c8bb8ced1055b5054100c388f7c"
@@ -4279,16 +4417,16 @@ cookie@0.5.0:
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
core-js-compat@^3.31.0, core-js-compat@^3.33.1:
- version "3.34.0"
- resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.34.0.tgz#61a4931a13c52f8f08d924522bba65f8c94a5f17"
- integrity sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA==
+ version "3.35.0"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.35.0.tgz#c149a3d1ab51e743bc1da61e39cb51f461a41873"
+ integrity sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==
dependencies:
browserslist "^4.22.2"
core-js-pure@^3.23.3:
- version "3.34.0"
- resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.34.0.tgz#981e462500708664c91b827a75b011f04a8134a0"
- integrity sha512-pmhivkYXkymswFfbXsANmBAewXx86UBfmagP+w0wkK06kLsLlTK5oQmsURPivzMkIBQiYq2cjamcZExIwlFQIg==
+ version "3.35.0"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.35.0.tgz#4660033304a050215ae82e476bd2513a419fbb34"
+ integrity sha512-f+eRYmkou59uh7BPcyJ8MC76DiGhspj1KMxVIcF24tzP8NA9HVa1uC7BTW2tgx7E1QVCzDzsgp7kArrzhlz8Ew==
core-js@^1.0.0:
version "1.2.7"
@@ -4296,9 +4434,9 @@ core-js@^1.0.0:
integrity sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==
core-js@^3.19.2:
- version "3.34.0"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.34.0.tgz#5705e6ad5982678612e96987d05b27c6c7c274a5"
- integrity sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==
+ version "3.35.0"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.35.0.tgz#58e651688484f83c34196ca13f099574ee53d6b4"
+ integrity sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==
core-util-is@~1.0.0:
version "1.0.3"
@@ -4463,9 +4601,9 @@ css.escape@^1.5.1:
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
cssdb@^7.1.0:
- version "7.9.1"
- resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.9.1.tgz#d76e06509dc1e11050836c3d556988fdbffa749e"
- integrity sha512-fqy6ZnNfpb8qAvTT0qijWyTsUmYThsDX2F2ctMG4ceI7mI4DtsMILSiMBiuuDnVIYTyWvCctdp9Nb08p/6m2SQ==
+ version "7.10.0"
+ resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.10.0.tgz#08816db7b793f088263e8f61dfe8d7f11a3459f2"
+ integrity sha512-yGZ5tmA57gWh/uvdQBHs45wwFY0IBh3ypABk5sEubPBPSzXzkNgsWReqx7gdx6uhC+QoFBe+V8JwBB9/hQ6cIA==
cssesc@^3.0.0:
version "3.0.0"
@@ -4550,6 +4688,77 @@ csstype@^3.0.2, csstype@^3.1.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+ integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+ dependencies:
+ internmap "1 - 2"
+
+"d3-color@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-ease@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+"d3-format@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+ integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+d3-path@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+ integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-scale@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+ integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+ dependencies:
+ d3-array "2.10.0 - 3"
+ d3-format "1 - 3"
+ d3-interpolate "1.2.0 - 3"
+ d3-time "2.1.1 - 3"
+ d3-time-format "2 - 4"
+
+d3-shape@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+ integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+ dependencies:
+ d3-path "^3.1.0"
+
+"d3-time-format@2 - 4":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+ integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+ dependencies:
+ d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+ integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+ dependencies:
+ d3-array "2 - 3"
+
+d3-timer@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -4564,6 +4773,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+date-fns@3.0.6:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.0.6.tgz#fe3aeb7592d359f075ffc41cb16139828810ca83"
+ integrity sha512-W+G99rycpKMMF2/YD064b2lE7jJGUe+EjOES7Q8BIGY8sbNdbgcs9XFTZwvzc9Jx1f3k7LB7gZaZa7f8Agzljg==
+
debug@2.6.9, debug@^2.6.0:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -4585,6 +4799,11 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
+decimal.js-light@^2.4.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
+ integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
+
decimal.js@^10.2.1:
version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
@@ -4772,6 +4991,13 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"
+dom-helpers@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
+ integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@@ -5376,7 +5602,7 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
-eventemitter3@^4.0.0:
+eventemitter3@^4.0.0, eventemitter3@^4.0.1:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
@@ -5469,6 +5695,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+fast-equals@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d"
+ integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==
+
fast-glob@^3.2.9, fast-glob@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
@@ -5618,9 +5849,9 @@ flatted@^3.2.9:
integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==
follow-redirects@^1.0.0, follow-redirects@^1.15.0:
- version "1.15.3"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
- integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
+ version "1.15.4"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
+ integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
for-each@^0.3.3:
version "0.3.3"
@@ -6208,6 +6439,11 @@ internal-slot@^1.0.4, internal-slot@^1.0.5:
hasown "^2.0.0"
side-channel "^1.0.4"
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -7297,6 +7533,16 @@ launch-editor@^2.6.0:
picocolors "^1.0.0"
shell-quote "^1.8.1"
+leaflet.heat@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/leaflet.heat/-/leaflet.heat-0.2.0.tgz#109d8cf586f0adee41f05aff031e27a77fecc229"
+ integrity sha512-Cd5PbAA/rX3X3XKxfDoUGi9qp78FyhWYurFg3nsfhntcM/MCNK08pRkf4iEenO1KNqwVPKCmkyktjW3UD+h9bQ==
+
+leaflet@1.9.4:
+ version "1.9.4"
+ resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
+ integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
+
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -7399,7 +7645,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
-lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
+lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -8575,9 +8821,9 @@ postcss-selector-not@^6.0.1:
postcss-selector-parser "^6.0.10"
postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9:
- version "6.0.14"
- resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.14.tgz#9d45f1afbebedae6811a17f49d09754f2ad153b3"
- integrity sha512-65xXYsT40i9GyWzlHQ5ShZoK7JZdySeOozi/tz2EezDo6c04q6+ckYMeoY7idaie1qp2dT5KoYQ2yky6JuoHnA==
+ version "6.0.15"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535"
+ integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
@@ -8710,7 +8956,7 @@ prop-types@15.5.10:
fbjs "^0.8.9"
loose-envify "^1.3.1"
-prop-types@^15.8.1:
+prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -8805,10 +9051,10 @@ react-app-polyfill@^3.0.0:
regenerator-runtime "^0.13.9"
whatwg-fetch "^3.6.2"
-react-day-picker@8.9.1:
- version "8.9.1"
- resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.9.1.tgz#62dcc2bc1282ac72d057266112d9c8558334e757"
- integrity sha512-W0SPApKIsYq+XCtfGeMYDoU0KbsG3wfkYtlw8l+vZp6KoBXGOlhzBUp4tNx1XiwiOZwhfdGOlj7NGSCKGSlg5Q==
+react-day-picker@8.10.0:
+ version "8.10.0"
+ resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.0.tgz#729c5b9564967a924213978fb9c0751884a60595"
+ integrity sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==
react-dev-utils@^12.0.1:
version "12.0.1"
@@ -8876,7 +9122,7 @@ react-ionicons@4.2.1:
react "17.0.1"
react-dom "17.0.1"
-react-is@^16.13.1:
+react-is@^16.10.2, react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -8891,6 +9137,18 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
+react-leaflet@4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-4.2.1.tgz#c300e9eccaf15cb40757552e181200aa10b94780"
+ integrity sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==
+ dependencies:
+ "@react-leaflet/core" "^2.1.0"
+
+react-lifecycles-compat@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
+ integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
+
react-redux@9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.0.2.tgz#5654d490be9abd34b73f369d3c1f89d6b14b072e"
@@ -9015,6 +9273,14 @@ react-slick@0.29.0:
lodash.debounce "^4.0.8"
resize-observer-polyfill "^1.5.0"
+react-smooth@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.5.tgz#d153b7dffc7143d0c99e82db1532f8cf93f20ecd"
+ integrity sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==
+ dependencies:
+ fast-equals "^5.0.0"
+ react-transition-group "2.9.0"
+
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
@@ -9024,6 +9290,16 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4"
tslib "^2.0.0"
+react-transition-group@2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
+ integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
+ dependencies:
+ dom-helpers "^3.4.0"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+ react-lifecycles-compat "^3.0.4"
+
react-uuid@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-uuid/-/react-uuid-2.0.0.tgz#e3c97e190db9eef53cb9dacfcc7314d913a411fa"
@@ -9080,6 +9356,27 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
+recharts-scale@^0.4.4:
+ version "0.4.5"
+ resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
+ integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
+ dependencies:
+ decimal.js-light "^2.4.1"
+
+recharts@2.10.3:
+ version "2.10.3"
+ resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.10.3.tgz#a5dbe219354d744701e8bbd116fe42393af92f6b"
+ integrity sha512-G4J96fKTZdfFQd6aQnZjo2nVNdXhp+uuLb00+cBTGLo85pChvm1+E67K3wBOHDE/77spcYb2Cy9gYWVqiZvQCg==
+ dependencies:
+ clsx "^2.0.0"
+ eventemitter3 "^4.0.1"
+ lodash "^4.17.19"
+ react-is "^16.10.2"
+ react-smooth "^2.0.5"
+ recharts-scale "^0.4.4"
+ tiny-invariant "^1.3.1"
+ victory-vendor "^36.6.8"
+
recursive-readdir@^2.2.2:
version "2.2.3"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372"
@@ -9147,9 +9444,9 @@ regenerator-transform@^0.15.2:
"@babel/runtime" "^7.8.4"
regex-parser@^2.2.11:
- version "2.2.11"
- resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58"
- integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.3.0.tgz#4bb61461b1a19b8b913f3960364bb57887f920ee"
+ integrity sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==
regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1:
version "1.5.1"
@@ -9753,7 +10050,6 @@ string-natural-compare@^3.0.1:
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0:
- name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9911,9 +10207,9 @@ stylehacks@^5.1.1:
postcss-selector-parser "^6.0.4"
stylis@^4.3.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.0.tgz#abe305a669fc3d8777e10eefcfc73ad861c5588c"
- integrity sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.1.tgz#ed8a9ebf9f76fe1e12d462f5cc3c4c980b23a7eb"
+ integrity sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==
sucrase@^3.32.0:
version "3.35.0"
@@ -10111,17 +10407,17 @@ terminal-link@^2.0.0:
supports-hyperlinks "^2.0.0"
terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.7:
- version "5.3.9"
- resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1"
- integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==
+ version "5.3.10"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199"
+ integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==
dependencies:
- "@jridgewell/trace-mapping" "^0.3.17"
+ "@jridgewell/trace-mapping" "^0.3.20"
jest-worker "^27.4.5"
schema-utils "^3.1.1"
serialize-javascript "^6.0.1"
- terser "^5.16.8"
+ terser "^5.26.0"
-terser@^5.0.0, terser@^5.10.0, terser@^5.16.8:
+terser@^5.0.0, terser@^5.10.0, terser@^5.26.0:
version "5.26.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.26.0.tgz#ee9f05d929f4189a9c28a0feb889d96d50126fe1"
integrity sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==
@@ -10169,6 +10465,11 @@ thunky@^1.0.2:
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
+tiny-invariant@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
+ integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
+
tmpl@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
@@ -10517,6 +10818,26 @@ vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+victory-vendor@^36.6.8:
+ version "36.7.0"
+ resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.7.0.tgz#e02af33e249e74e659fa65c6d5936042c42e7aa8"
+ integrity sha512-nqYuTkLSdTTeACyXcCLbL7rl0y6jpzLPtTNGOtSnajdR+xxMxBdjMxDjfNJNlhR+ZU8vbXz+QejntcbY7h9/ZA==
+ dependencies:
+ "@types/d3-array" "^3.0.3"
+ "@types/d3-ease" "^3.0.0"
+ "@types/d3-interpolate" "^3.0.1"
+ "@types/d3-scale" "^4.0.2"
+ "@types/d3-shape" "^3.1.0"
+ "@types/d3-time" "^3.0.0"
+ "@types/d3-timer" "^3.0.0"
+ d3-array "^3.1.6"
+ d3-ease "^3.0.1"
+ d3-interpolate "^3.0.1"
+ d3-scale "^4.0.2"
+ d3-shape "^3.1.0"
+ d3-time "^3.0.0"
+ d3-timer "^3.0.1"
+
w3c-hr-time@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
@@ -10967,7 +11288,6 @@ workbox-window@6.6.1:
workbox-core "6.6.1"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
- name wrap-ansi-cjs
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -11063,6 +11383,11 @@ yargs@^16.2.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
+yarn@1.22.21:
+ version "1.22.21"
+ resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.21.tgz#1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
+ integrity sha512-ynXaJsADJ9JiZ84zU25XkPGOvVMmZ5b7tmTSpKURYwgELdjucAOydqIOrOfTxVYcNXe91xvLZwcRh68SR3liCg==
+
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"