add i18n and popover for brand url

This commit is contained in:
Josh Hawkins 2025-10-12 12:51:23 -05:00
parent 84ac8d3654
commit c2183bd850
3 changed files with 46 additions and 11 deletions

View File

@ -164,7 +164,7 @@
"step1": { "step1": {
"description": "Enter your camera details and test the connection.", "description": "Enter your camera details and test the connection.",
"cameraName": "Camera Name", "cameraName": "Camera Name",
"cameraNamePlaceholder": "e.g., front_door", "cameraNamePlaceholder": "e.g., front_door or Back Yard Overview",
"host": "Host/IP Address", "host": "Host/IP Address",
"port": "Port", "port": "Port",
"username": "Username", "username": "Username",
@ -175,6 +175,8 @@
"cameraBrand": "Camera Brand", "cameraBrand": "Camera Brand",
"selectBrand": "Select camera brand for URL template", "selectBrand": "Select camera brand for URL template",
"customUrl": "Custom Stream URL", "customUrl": "Custom Stream URL",
"brandInformation": "Brand information",
"brandUrlFormat": "For cameras with the RTSP URL format as: {{exampleUrl}}",
"customUrlPlaceholder": "rtsp://username:password@host:port/path", "customUrlPlaceholder": "rtsp://username:password@host:port/path",
"testConnection": "Test Connection", "testConnection": "Test Connection",
"testSuccess": "Connection test successful!", "testSuccess": "Connection test successful!",
@ -184,7 +186,11 @@
"noSnapshot": "Unable to fetch a snapshot from the configured stream." "noSnapshot": "Unable to fetch a snapshot from the configured stream."
}, },
"errors": { "errors": {
"brandOrCustomUrlRequired": "Either select a camera brand with host/IP or choose 'Other' with a custom URL" "brandOrCustomUrlRequired": "Either select a camera brand with host/IP or choose 'Other' with a custom URL",
"nameRequired": "Camera name is required",
"nameLength": "Camera name must be 64 characters or less",
"invalidCharacters": "Camera name contains invalid characters",
"nameExists": "Camera name already exists"
} }
}, },
"step2": { "step2": {
@ -292,8 +298,8 @@
"description": "Configure camera settings including stream inputs and roles.", "description": "Configure camera settings including stream inputs and roles.",
"name": "Camera Name", "name": "Camera Name",
"nameRequired": "Camera name is required", "nameRequired": "Camera name is required",
"nameLength": "Camera name must be less than 24 characters.", "nameLength": "Camera name must be less than 64 characters.",
"namePlaceholder": "e.g., front_door", "namePlaceholder": "e.g., front_door or Back Yard Overview",
"enabled": "Enabled", "enabled": "Enabled",
"ffmpeg": { "ffmpeg": {
"inputs": "Input Streams", "inputs": "Input Streams",

View File

@ -39,6 +39,12 @@ import {
} from "@/types/cameraWizard"; } from "@/types/cameraWizard";
import { FaCircleCheck } from "react-icons/fa6"; import { FaCircleCheck } from "react-icons/fa6";
import { Card, CardContent, CardTitle } from "../ui/card"; import { Card, CardContent, CardTitle } from "../ui/card";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { LuInfo } from "react-icons/lu";
type Step1NameCameraProps = { type Step1NameCameraProps = {
wizardData: Partial<WizardFormData>; wizardData: Partial<WizardFormData>;
@ -71,12 +77,15 @@ export default function Step1NameCamera({
.object({ .object({
cameraName: z cameraName: z
.string() .string()
.min(1, "Camera name is required") .min(1, t("cameraWizard.step1.errors.nameRequired"))
.max(64, "Camera name must be 64 characters or less") .max(64, t("cameraWizard.step1.errors.nameLength"))
.regex(/^[a-zA-Z0-9\s_-]+$/, "Camera name contains invalid characters") .regex(
/^[a-zA-Z0-9\s_-]+$/,
t("cameraWizard.step1.errors.invalidCharacters"),
)
.refine( .refine(
(value) => !existingCameraNames.includes(value), (value) => !existingCameraNames.includes(value),
"Camera name already exists", t("cameraWizard.step1.errors.nameExists"),
), ),
host: z.string().optional(), host: z.string().optional(),
username: z.string().optional(), username: z.string().optional(),
@ -357,9 +366,29 @@ export default function Step1NameCamera({
const selectedBrand = CAMERA_BRANDS.find( const selectedBrand = CAMERA_BRANDS.find(
(brand) => brand.value === field.value, (brand) => brand.value === field.value,
); );
return selectedBrand ? ( return selectedBrand &&
selectedBrand.value != "other" ? (
<FormDescription className="mt-1 pt-0.5 text-xs text-muted-foreground"> <FormDescription className="mt-1 pt-0.5 text-xs text-muted-foreground">
{selectedBrand.exampleUrl} <Popover>
<PopoverTrigger>
<div className="flex flex-row items-center gap-0.5 text-xs text-muted-foreground hover:text-primary">
<LuInfo className="mr-1 size-3" />
{t("cameraWizard.step1.brandInformation")}
</div>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-2">
<h4 className="font-medium">
{selectedBrand.label}
</h4>
<p className="text-sm text-muted-foreground">
{t("cameraWizard.step1.brandUrlFormat", {
exampleUrl: selectedBrand.exampleUrl,
})}
</p>
</div>
</PopoverContent>
</Popover>
</FormDescription> </FormDescription>
) : null; ) : null;
})()} })()}

View File

@ -226,7 +226,7 @@ export default function Step2StreamConfig({
{[ {[
stream.testResult.resolution, stream.testResult.resolution,
stream.testResult.fps stream.testResult.fps
? `${stream.testResult.fps} fps` ? `${stream.testResult.fps} ${t("cameraWizard.testResultLabels.fps")}`
: null, : null,
stream.testResult.videoCodec, stream.testResult.videoCodec,
stream.testResult.audioCodec, stream.testResult.audioCodec,