generators) {
+ this.generators = generators;
+ }
+
+ public String resolveAndGenerate(UrlRequestDTO urlRequestDTO) {
+ logger.debug("Inside the resolveAndGenerate, ");
+ if (urlRequestDTO == null || urlRequestDTO.fullUrl() == null || urlRequestDTO.fullUrl().isEmpty()) {
+ logger.error("Invalid parameters , request or actual url cannot be null or empty ");
+ throw new IllegalParametersException("Invalid parameters , request or actual url cannot be null or empty");
+ }
+ return generators
+ .stream()
+ .filter(gen -> gen.supports(urlRequestDTO))
+ .findFirst()
+ .map(gen -> gen.generate(urlRequestDTO))
+ .orElseThrow(() -> new ConfigMissingException("No suitable alias generator found!"));
+ }
+
+}
diff --git a/backend/src/main/java/com/tpx/urlshort/service/alias/Base62Encoder.java b/backend/src/main/java/com/tpx/urlshort/service/alias/Base62Encoder.java
new file mode 100644
index 00000000..1fd9a906
--- /dev/null
+++ b/backend/src/main/java/com/tpx/urlshort/service/alias/Base62Encoder.java
@@ -0,0 +1,59 @@
+package com.tpx.urlshort.service.alias;
+
+import com.tpx.urlshort.exception.IllegalParametersException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * To generate a 7 length long alphabet
+ */
+@Component
+public class Base62Encoder {
+
+ private static final Logger logger = LoggerFactory.getLogger(Base62Encoder.class);
+ private static final char[] ALPHA_NUMERICS_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ .toCharArray();
+ private static final Integer ALPHA_NUMERICS_LENGTH = ALPHA_NUMERICS_CHARS.length;
+ private static final int FIXED_SIZE_OF_SHORTENED_URL = 7;
+ private static final long TOKEN_THRESHOLD = 3521614606207L; // 62^7 - 1
+
+ public String encode(long number) {
+
+ logger.debug("Encoding started for {}", number);
+
+ if (number < 2) {
+ String message = String.format("Invalid input too less to consider %d", number);
+ logger.error(message);
+ throw new IllegalParametersException(message);
+ }
+
+ if (number > TOKEN_THRESHOLD) {
+ String message = String.format("Value %d exceeds maximum capacity for %d chars ( %d )", number,
+ FIXED_SIZE_OF_SHORTENED_URL, TOKEN_THRESHOLD);
+ logger.error(message);
+ throw new IllegalParametersException(message);
+
+ }
+
+ char[] numCharHolder = new char[FIXED_SIZE_OF_SHORTENED_URL];
+ int placer = FIXED_SIZE_OF_SHORTENED_URL;
+
+ while (number > 0) {
+ int index = (int) (number % ALPHA_NUMERICS_LENGTH);
+ numCharHolder[--placer] = ALPHA_NUMERICS_CHARS[index];
+ number = number / ALPHA_NUMERICS_LENGTH;
+ }
+
+ // To keep the generated shortened url fixed length
+ while (placer > 0) {
+ // filler will be 'a',
+ numCharHolder[--placer] = ALPHA_NUMERICS_CHARS[10];
+ }
+
+ String finalEncodedValue = new String(numCharHolder);
+ logger.debug("Encoded completed for {} , final value is {} ", number, finalEncodedValue);
+ return finalEncodedValue;
+ }
+
+}
diff --git a/backend/src/main/java/com/tpx/urlshort/service/alias/CustomAliasGenerator.java b/backend/src/main/java/com/tpx/urlshort/service/alias/CustomAliasGenerator.java
new file mode 100644
index 00000000..b584ad35
--- /dev/null
+++ b/backend/src/main/java/com/tpx/urlshort/service/alias/CustomAliasGenerator.java
@@ -0,0 +1,22 @@
+package com.tpx.urlshort.service.alias;
+
+import com.tpx.urlshort.dto.UrlRequestDTO;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CustomAliasGenerator implements AliasGenerator {
+
+
+ @Override
+ public String generate(UrlRequestDTO requestDTO) {
+ if (requestDTO.customAlias() == null || requestDTO.customAlias().isEmpty()) {
+ throw new IllegalArgumentException("Custom alias cannot be null or empty");
+ }
+ return requestDTO.customAlias().trim();
+ }
+
+ @Override
+ public boolean supports(UrlRequestDTO requestDTO) {
+ return requestDTO.customAlias() != null && !requestDTO.customAlias().isBlank();
+ }
+}
diff --git a/backend/src/main/java/com/tpx/urlshort/service/alias/SnowflakeAliasGenerator.java b/backend/src/main/java/com/tpx/urlshort/service/alias/SnowflakeAliasGenerator.java
new file mode 100644
index 00000000..de88f523
--- /dev/null
+++ b/backend/src/main/java/com/tpx/urlshort/service/alias/SnowflakeAliasGenerator.java
@@ -0,0 +1,36 @@
+package com.tpx.urlshort.service.alias;
+
+import com.tpx.urlshort.dto.UrlRequestDTO;
+import com.tpx.urlshort.service.alias.snowflake.SnowflakeIdGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SnowflakeAliasGenerator implements AliasGenerator {
+
+ private static final Logger logger = LoggerFactory.getLogger(SnowflakeAliasGenerator.class);
+ private final SnowflakeIdGenerator snowflakeIdGenerator;
+ private final Base62Encoder encoder;
+
+ public SnowflakeAliasGenerator(SnowflakeIdGenerator snowflakeIdGenerator, Base62Encoder encoder) {
+ this.snowflakeIdGenerator = snowflakeIdGenerator;
+ this.encoder = encoder;
+
+ }
+
+ @Override
+ public String generate(UrlRequestDTO requestDTO) {
+ long nextId = snowflakeIdGenerator.getNextId();
+ logger.debug("The id -{}- is generated for the url -{}-", nextId, requestDTO.fullUrl());
+ String finalAlias = encoder.encode(nextId);
+ logger.debug("The final alias -{}- is generated for the url -{}-", finalAlias, requestDTO.fullUrl());
+ return finalAlias;
+ }
+
+ @Override
+ public boolean supports(UrlRequestDTO requestDTO) {
+ logger.debug("Inside supports");
+ return requestDTO == null || requestDTO.customAlias() == null || requestDTO.customAlias().isBlank();
+ }
+}
diff --git a/backend/src/main/java/com/tpx/urlshort/service/alias/snowflake/SnowflakeIdGenerator.java b/backend/src/main/java/com/tpx/urlshort/service/alias/snowflake/SnowflakeIdGenerator.java
new file mode 100644
index 00000000..baea74d9
--- /dev/null
+++ b/backend/src/main/java/com/tpx/urlshort/service/alias/snowflake/SnowflakeIdGenerator.java
@@ -0,0 +1,121 @@
+package com.tpx.urlshort.service.alias.snowflake;
+
+import com.tpx.urlshort.exception.IllegalParametersException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.time.Instant;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+@Component
+public class SnowflakeIdGenerator {
+
+ /**
+ * The combination of timestamp difference ( (from a reference value - current )
+ * in sec)
+ * + the workstation id + dataCenterId + a generating sequence number
+ * ensure the uniqueness at each server level
+ *
+ * Why this complicated logic is required..
+ * -> There is no need for using any external system for uniqueness
+ * -> Scalable
+ * -> uniqueness is guaranteed at the server level
+ * -> Standard approach used in the industry
+ */
+
+ private static final Logger logger = LoggerFactory.getLogger(SnowflakeIdGenerator.class);
+ // Bit distribution to fit exactly under 62^7 (Max 41 bits total)
+ private static final long DATA_CENTER_ID_BITS = 2L;
+ private static final long WORKER_ID_BITS = 3L;
+ private static final long SEQUENCE_BITS = 5L;
+ private static final long MAX_DATA_CENTER_ID = (1L << DATA_CENTER_ID_BITS) - 1; // 3
+ private static final long MAX_WORKER_ID = (1L << WORKER_ID_BITS) - 1; // 7
+ private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1; // 31
+ private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
+ private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
+ private static final long TIMESTAMP_SHIFT = DATA_CENTER_ID_SHIFT + DATA_CENTER_ID_BITS;
+ private final long workerId;
+ private final long dataCenterId;
+ private final long timestampBaseReference;
+ private final Lock lock = new ReentrantLock();
+ private long sequence = 0L;
+ private long lastTimestamp = -1L;
+
+ public SnowflakeIdGenerator(@Value("${snowflake.worker-id:0}") long workerId,
+ @Value("${snowflake.datacenter-id:0}") long dataCenterId,
+ @Value("${snowflake.epoch-reference:1767225600}") long timestampBaseReference) {
+
+ if (workerId < 0 || workerId > MAX_WORKER_ID) {
+ throw new IllegalParametersException("Worker ID must be between 0 and " + MAX_WORKER_ID);
+ }
+ if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {
+ throw new IllegalParametersException("Data Center ID must be between 0 and " + MAX_DATA_CENTER_ID);
+ }
+
+ this.workerId = workerId;
+ this.dataCenterId = dataCenterId;
+ this.timestampBaseReference = timestampBaseReference;
+ }
+
+ public long getNextId() {
+
+ try {
+ logger.debug("Next Id requested");
+ lock.lock();
+
+ // calculate the current timestamp..
+ long currentTimeStamp = getCurrentTimestamp();
+
+ // the current timestamp is less than previous , that means the clock is reset
+ if (currentTimeStamp < lastTimestamp) {
+ String message = String.format("Clock moved backwards. Refusing to generate id for %s seconds.",
+ (lastTimestamp - currentTimeStamp));
+ logger.error(message);
+ throw new IllegalStateException(message);
+ }
+
+ // if sequence are generate at the same time, increment it
+ logger.debug("Calculating current timestamp and sequence");
+ if (currentTimeStamp == lastTimestamp) {
+ // if the sequence reached the max seq, reset it to 0 and get a new timestamp in
+ // future
+ sequence = (sequence + 1) & MAX_SEQUENCE;
+ if (sequence == 0L) {
+ // Sequence exhausted for this second, block until the clock ticks forward
+ currentTimeStamp = waitAndGetNextTimestamp(currentTimeStamp);
+ }
+ } else {
+ // if timestamp is greater use 0
+ sequence = 0L;
+ }
+ logger.debug("current timestamp and sequence {} , {} ", currentTimeStamp, sequence);
+ lastTimestamp = currentTimeStamp;
+
+ // Compound 41-bit ID calculation
+ long nextIdGenerated = ((currentTimeStamp - timestampBaseReference) << TIMESTAMP_SHIFT)
+ | (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence;
+ logger.debug("nextIdGenerated {}", nextIdGenerated);
+ return nextIdGenerated;
+
+ } finally {
+ lock.unlock();
+ }
+
+ }
+
+ long getCurrentTimestamp() {
+ return Instant.now().getEpochSecond(); // Extracted in seconds
+ }
+
+ long waitAndGetNextTimestamp(long lastTimestamp) {
+ long timestamp = getCurrentTimestamp();
+ while (timestamp <= lastTimestamp) {
+ timestamp = getCurrentTimestamp();
+ }
+ return timestamp;
+ }
+
+}
diff --git a/backend/src/main/resources/application-docker.yml b/backend/src/main/resources/application-docker.yml
new file mode 100644
index 00000000..1a9d058e
--- /dev/null
+++ b/backend/src/main/resources/application-docker.yml
@@ -0,0 +1,35 @@
+# Enable Java 21 Virtual Threads for embedded Tomcat
+spring:
+ threads:
+ virtual:
+ enabled: true
+ jpa:
+ show-sql: false
+ hibernate:
+ ddl-auto: update
+ datasource:
+ driver-class-name: org.sqlite.JDBC
+ url: jdbc:sqlite:/app/data/url_shortener.db
+ data:
+ redis:
+ host: host.docker.internal
+ port: 6379
+ timeout: 2000ms
+
+springdoc:
+ swagger-ui:
+ enabled: false
+ api-docs:
+ enabled: false
+
+app:
+ base-url: "${APP_BASE_URL:http://localhost:8585/api/v1/}"
+ cors:
+ allowed-origins:
+ - "${CORS_ALLOWED_ORIGINS:http://localhost}"
+ - "http://localhost:80"
+
+logging:
+ level:
+ root: WARN
+ com.tpx.urlshort: INFO
diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml
new file mode 100644
index 00000000..bf5930cf
--- /dev/null
+++ b/backend/src/main/resources/application-local.yml
@@ -0,0 +1,34 @@
+spring:
+ jpa:
+ show-sql: true
+ hibernate:
+ ddl-auto: update
+ datasource:
+ driver-class-name: org.sqlite.JDBC
+ url: jdbc:sqlite:data/url_shortener.db
+ data:
+ redis:
+ host: localhost
+ port: 6379
+ timeout: 2000ms
+
+springdoc:
+ swagger-ui:
+ enabled: true
+ api-docs:
+ enabled: true
+ path: /api/v1/api-docs
+
+app:
+ base-url: http://localhost:8585/api/v1/
+ cors:
+ allowed-origins:
+ - http://localhost:5173
+ - http://localhost:5174
+
+logging:
+ level:
+ root: INFO
+ com.tpx.urlshort: DEBUG
+ org.springframework.web: DEBUG
+
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
new file mode 100644
index 00000000..65219294
--- /dev/null
+++ b/backend/src/main/resources/application.yml
@@ -0,0 +1,61 @@
+spring:
+ application:
+ name: "TPX Impact URL Shortener"
+ profiles:
+ active:
+ - local
+ flyway:
+ clean-disabled: true
+ baseline-on-migrate: true
+ enabled: true
+ jpa:
+ hibernate:
+ ddl-auto: update
+ database-platform: org.hibernate.community.dialect.SQLiteDialect
+
+server:
+ port: 8585
+ tomcat:
+ max-http-post-size: 1MB
+ max-http-request-header-size: 8KB
+
+springdoc:
+ packages-to-scan: com.tpx.urlshort.controller
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health,info,metrics
+ endpoint:
+ health:
+ show-details: always
+ probes:
+ enabled: true
+ metrics:
+ enable:
+ jvm: true
+
+snowflake:
+ worker-id: 2
+ datacenter-id: 2
+ epoch-reference: 1767225600
+
+app:
+ cache:
+ ttl-seconds: 600
+ cors:
+ allowed-methods:
+ - GET
+ - POST
+ - PUT
+ - DELETE
+ - OPTIONS
+ allowed-headers:
+ - "*"
+ allow-credentials: true
+
+logging:
+ pattern:
+ console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
+ file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
diff --git a/backend/src/test/java/com/tpx/urlshort/controller/UrlControllerIT.java b/backend/src/test/java/com/tpx/urlshort/controller/UrlControllerIT.java
new file mode 100644
index 00000000..0dbe8fee
--- /dev/null
+++ b/backend/src/test/java/com/tpx/urlshort/controller/UrlControllerIT.java
@@ -0,0 +1,169 @@
+package com.tpx.urlshort.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.tpx.urlshort.cache.redis.UrlCacheService;
+import com.tpx.urlshort.domain.UrlDetails;
+import com.tpx.urlshort.dto.UrlRequestDTO;
+import com.tpx.urlshort.repository.UrlRepository;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class UrlControllerIT {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ private UrlRepository urlRepository;
+
+ @MockBean
+ private UrlCacheService urlCacheService;
+
+ @BeforeEach
+ void cleanUp() {
+ urlRepository.deleteAll();
+ // Mock cache to always return empty (cache miss) - forces DB lookup
+ when(urlCacheService.get(anyString())).thenReturn(Optional.empty());
+ }
+
+ @Test
+ void shortenUrl_shouldReturn201AndPersist() throws Exception {
+ UrlRequestDTO request = new UrlRequestDTO("https://example.com/some/path", "mycustomalias");
+
+ mockMvc.perform(post("/api/v1/shorten")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.shortUrl").value("http://localhost:8080/mycustomalias"))
+ .andExpect(jsonPath("$.actualUrl").value("https://example.com/some/path"));
+
+ Assertions.assertTrue(urlRepository.findByShortUrl("mycustomalias").isPresent());
+ }
+
+ @Test
+ void shortenUrl_whenCustomAliasHasSpecialCharacters_shouldReturn400() throws Exception {
+ UrlRequestDTO request = new UrlRequestDTO("https://example.com/some/path", "my alias!");
+
+ mockMvc.perform(post("/api/v1/shorten")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void shortenUrl_whenAliasAlreadyExists_shouldReturn400() throws Exception {
+ UrlDetails existing = new UrlDetails();
+ existing.setActualUrl("https://already.com");
+ existing.setShortUrl("dupalias");
+ urlRepository.saveAndFlush(existing);
+
+ UrlRequestDTO request = new UrlRequestDTO("https://example.com/new", "dupalias");
+
+ mockMvc.perform(post("/api/v1/shorten")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.status").value(400))
+ .andExpect(jsonPath("$.error").value("Invalid input or alias already taken"));
+ }
+
+ @Test
+ void redirectToFullUrl_shouldReturn302AndLocation() throws Exception {
+ UrlDetails existing = new UrlDetails();
+ existing.setActualUrl("https://lookup.com/path");
+ existing.setShortUrl("lookup-alias");
+ urlRepository.saveAndFlush(existing);
+
+ mockMvc.perform(get("/api/v1/lookup-alias"))
+ .andExpect(status().isFound())
+ .andExpect(header().string("Location", "https://lookup.com/path"));
+ }
+
+ @Test
+ void redirectToFullUrl_whenAliasMissing_shouldReturn404() throws Exception {
+ mockMvc.perform(get("/api/v1/missing-alias"))
+ .andExpect(status().isNotFound())
+ .andExpect(jsonPath("$.status").value(404))
+ .andExpect(jsonPath("$.error").value("Alias not found"));
+ }
+
+ @Test
+ void deleteUrl_shouldReturn204AndRemoveEntry() throws Exception {
+ UrlDetails existing = new UrlDetails();
+ existing.setActualUrl("https://delete-me.com");
+ existing.setShortUrl("to-delete");
+ urlRepository.saveAndFlush(existing);
+
+ mockMvc.perform(delete("/api/v1/to-delete"))
+ .andExpect(status().isNoContent());
+
+ Assertions.assertTrue(urlRepository.findByShortUrl("to-delete").isEmpty());
+ }
+
+ @Test
+ void deleteUrl_whenAliasMissing_shouldReturn404() throws Exception {
+ mockMvc.perform(delete("/api/v1/not-here"))
+ .andExpect(status().isNotFound())
+ .andExpect(jsonPath("$.status").value(404))
+ .andExpect(jsonPath("$.error").value("Alias not found"));
+ }
+
+ @Test
+ void listAllUrls_shouldReturn200AndPagedContent() throws Exception {
+ UrlDetails first = new UrlDetails();
+ first.setActualUrl("https://site1.com");
+ first.setShortUrl("AB");
+ first.setCreatedAt(LocalDateTime.of(2026, 8, 26, 11, 55, 0));
+ first.setUpdatedAt(first.getCreatedAt());
+
+ UrlDetails second = new UrlDetails();
+ second.setActualUrl("https://site2.com");
+ second.setShortUrl("pq");
+ second.setCreatedAt(LocalDateTime.of(2026, 8, 26, 11, 58, 0));
+ second.setUpdatedAt(second.getCreatedAt());
+
+ UrlDetails third = new UrlDetails();
+ third.setActualUrl("https://site3.com");
+ third.setShortUrl("xy");
+ third.setCreatedAt(LocalDateTime.of(2026, 8, 26, 11, 59, 0));
+ third.setUpdatedAt(third.getCreatedAt());
+
+ urlRepository.saveAll(java.util.List.of(first, second, third));
+
+ mockMvc.perform(get("/api/v1/urls")
+ .param("page", "0")
+ .param("size", "2"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.content[0].shortUrl").value("http://localhost:8080/xy"))
+ .andExpect(jsonPath("$.content[0].actualUrl").value("https://site3.com"))
+ .andExpect(jsonPath("$.content[1].shortUrl").value("http://localhost:8080/pq"))
+ .andExpect(jsonPath("$.content[1].actualUrl").value("https://site2.com"))
+ .andExpect(jsonPath("$.totalElements").value(3));
+ }
+}
diff --git a/backend/src/test/java/com/tpx/urlshort/controller/UrlControllerTest.java b/backend/src/test/java/com/tpx/urlshort/controller/UrlControllerTest.java
new file mode 100644
index 00000000..71d730a5
--- /dev/null
+++ b/backend/src/test/java/com/tpx/urlshort/controller/UrlControllerTest.java
@@ -0,0 +1,130 @@
+package com.tpx.urlshort.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.tpx.urlshort.dto.UrlRequestDTO;
+import com.tpx.urlshort.dto.UrlResponseDTO;
+import com.tpx.urlshort.exception.AliasAlreadyPresentException;
+import com.tpx.urlshort.exception.ItemNotFoundException;
+import com.tpx.urlshort.service.UrlService;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(UrlController.class)
+class UrlControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @MockBean
+ private UrlService urlService;
+
+ @Test
+ void shortenUrl_shouldReturn201AndBody() throws Exception {
+ String fullUrl = "https://example.com/some/path";
+ String alias = "mycustomalias";
+ String expectedShortUrl = "http://localhost:8080/mycustomalias";
+ UrlRequestDTO request = new UrlRequestDTO(fullUrl, alias);
+ UrlResponseDTO response = new UrlResponseDTO(expectedShortUrl, fullUrl);
+ Mockito.when(urlService.shortenAndPersistURL(request)).thenReturn(response);
+ mockMvc.perform(post("/api/v1/shorten").contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request))).andExpect(status().isCreated())
+ .andExpect(jsonPath("$.shortUrl").value(expectedShortUrl))
+ .andExpect(jsonPath("$.actualUrl").value(fullUrl));
+ }
+
+ @Test
+ void shortenUrl_whenAliasTaken_shouldReturn400() throws Exception {
+ String fullUrl = "https://example.com/some/path";
+ String alias = "mycustomalias";
+ UrlRequestDTO request = new UrlRequestDTO(fullUrl, alias);
+ Mockito.when(urlService.shortenAndPersistURL(request))
+ .thenThrow(new AliasAlreadyPresentException("The alias -mycustomalias already exist"));
+
+ mockMvc.perform(post("/api/v1/shorten").contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request))).andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.status").value(400))
+ .andExpect(jsonPath("$.error").value("Invalid input or alias already taken"))
+ .andExpect(jsonPath("$.message").value("The alias -mycustomalias already exist"));
+ }
+
+ @Test
+ void shortenUrl_whenCustomAliasHasSpecialCharacters_shouldReturn400() throws Exception {
+ String fullUrl = "https://example.com/some/path";
+ UrlRequestDTO request = new UrlRequestDTO(fullUrl, "my alias!");
+
+ mockMvc.perform(post("/api/v1/shorten").contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void redirectToFullUrl_shouldReturn302AndLocationHeader() throws Exception {
+ String shortUrl = "http://localhost:8080/mycustomalias";
+ String actualUrl = "https://example.com/some/path";
+ String alias = "mycustomalias";
+ UrlResponseDTO response = new UrlResponseDTO(shortUrl, actualUrl);
+ Mockito.when(urlService.findByAlias(alias)).thenReturn(response);
+ mockMvc.perform(get("/api/v1/mycustomalias")).andExpect(status().isFound())
+ .andExpect(header().string("Location", actualUrl));
+ }
+
+ @Test
+ void redirectToFullUrl_whenAliasMissing_shouldReturn404() throws Exception {
+ Mockito.when(urlService.findByAlias("missing-alias"))
+ .thenThrow(new ItemNotFoundException("No details found for the alias missing-alias"));
+ mockMvc.perform(get("/api/v1/missing-alias")).andExpect(status().isNotFound())
+ .andExpect(jsonPath("$.status").value(404)).andExpect(jsonPath("$.error").value("Alias not found"))
+ .andExpect(jsonPath("$.message").value("No details found for the alias missing-alias"));
+ }
+
+ @Test
+ void deleteUrl_shouldReturn204() throws Exception {
+ mockMvc.perform(delete("/api/v1/to-delete")).andExpect(status().isNoContent());
+ Mockito.verify(urlService).delete("to-delete");
+ }
+
+ @Test
+ void deleteUrl_whenAliasMissing_shouldReturn404() throws Exception {
+
+ Mockito.doThrow(new ItemNotFoundException("No details found for the alias not-here")).when(urlService)
+ .delete("not-here");
+ mockMvc.perform(delete("/api/v1/not-here"))
+ .andExpect(status().isNotFound())
+ .andExpect(jsonPath("$.status").value(404))
+ .andExpect(jsonPath("$.error").value("Alias not found"))
+ .andExpect(jsonPath("$.message").value("No details found for the alias not-here"));
+ }
+
+ @Test
+ void listAllUrls_shouldReturn200AndPagedBody() throws Exception {
+ List content = List.of(
+ new UrlResponseDTO("http://localhost:8080/xy", "https://site1.com"),
+ new UrlResponseDTO("http://localhost:8080/ab", "https://site2.com"));
+ Page page = new PageImpl<>(content, PageRequest.of(0, 10), 2);
+ Mockito.when(urlService.getAll(Mockito.any())).thenReturn(page);
+
+ mockMvc.perform(get("/api/v1/urls").param("page", "0").param("size", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.content[0].shortUrl").value("http://localhost:8080/xy"))
+ .andExpect(jsonPath("$.content[0].actualUrl").value("https://site1.com"))
+ .andExpect(jsonPath("$.content[1].shortUrl").value("http://localhost:8080/ab"))
+ .andExpect(jsonPath("$.content[1].actualUrl").value("https://site2.com"))
+ .andExpect(jsonPath("$.totalElements").value(2));
+ }
+}
diff --git a/backend/src/test/java/com/tpx/urlshort/repository/UrlRepositoryIT.java b/backend/src/test/java/com/tpx/urlshort/repository/UrlRepositoryIT.java
new file mode 100644
index 00000000..9c7cc433
--- /dev/null
+++ b/backend/src/test/java/com/tpx/urlshort/repository/UrlRepositoryIT.java
@@ -0,0 +1,138 @@
+package com.tpx.urlshort.repository;
+
+import com.tpx.urlshort.domain.UrlDetails;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * This test class is created for future reason
+ * if someone is creating any api using complex custom sql
+ */
+@DataJpaTest
+//to stop defaulting to H2
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+class UrlRepositoryIT {
+
+
+ @Autowired
+ UrlRepository urlRepository;
+
+ @Test
+ void testSave() {
+ UrlDetails urlDetails = new UrlDetails();
+ urlDetails.setActualUrl("abcd");
+ urlDetails.setShortUrl("ab");
+ UrlDetails persistedUrlDetails = urlRepository.save(urlDetails);
+ Assertions.assertNotNull(persistedUrlDetails.getId());
+ Assertions.assertNotNull(persistedUrlDetails.getCreatedAt());
+ Assertions.assertNotNull(persistedUrlDetails.getUpdatedAt());
+ Assertions.assertEquals("abcd", persistedUrlDetails.getActualUrl());
+ Assertions.assertEquals("ab", persistedUrlDetails.getShortUrl());
+ }
+
+ @Test
+ void testGetById() {
+ UrlDetails urlDetails = new UrlDetails();
+ urlDetails.setActualUrl("abcd");
+ urlDetails.setShortUrl("ab");
+ UrlDetails persistedUrlDetails = urlRepository.save(urlDetails);
+ Assertions.assertNotNull(persistedUrlDetails.getId());
+ Assertions.assertNotNull(persistedUrlDetails.getCreatedAt());
+ Assertions.assertNotNull(persistedUrlDetails.getUpdatedAt());
+ Assertions.assertEquals("abcd", persistedUrlDetails.getActualUrl());
+ Assertions.assertEquals("ab", persistedUrlDetails.getShortUrl());
+ Optional resultById = urlRepository.findById(persistedUrlDetails.getId());
+ if (resultById.isEmpty()) {
+ Assertions.fail("Expected persisted value , but found none");
+ }
+ UrlDetails urlDetailsFound = resultById.get();
+ Assertions.assertEquals("abcd", urlDetailsFound.getActualUrl());
+ Assertions.assertEquals("ab", urlDetailsFound.getShortUrl());
+ Assertions.assertNotNull(persistedUrlDetails.getId());
+ }
+
+ @Test
+ void testDelete() {
+ UrlDetails urlDetails = new UrlDetails();
+ urlDetails.setActualUrl("abcd");
+ urlDetails.setShortUrl("ab");
+ UrlDetails persistedUrlDetails = urlRepository.save(urlDetails);
+ Assertions.assertNotNull(persistedUrlDetails.getId());
+ Assertions.assertNotNull(persistedUrlDetails.getCreatedAt());
+ Assertions.assertNotNull(persistedUrlDetails.getUpdatedAt());
+ Assertions.assertEquals("abcd", persistedUrlDetails.getActualUrl());
+ Assertions.assertEquals("ab", persistedUrlDetails.getShortUrl());
+ //start deleting
+ urlRepository.deleteById(persistedUrlDetails.getId());
+ Optional resultById = urlRepository.findById(persistedUrlDetails.getId());
+ Assertions.assertTrue(resultById.isEmpty());
+ }
+
+ @Test
+ void testGetAll_paginated() {
+ List urlDetailsLst = getUrlDetails();
+ urlRepository.saveAll(urlDetailsLst);
+ Pageable pageable = PageRequest.of(0, 2);
+ Page allResp = urlRepository.findAllByOrderByCreatedAtDescShortUrlAsc(pageable);
+ Assertions.assertNotNull(allResp);
+ Assertions.assertEquals(2, allResp.getContent().size());
+ Assertions.assertEquals(2, allResp.getTotalPages());
+ Assertions.assertEquals(4, allResp.getTotalElements());
+ }
+
+ @Test
+ void testFindByShortUrl() {
+ UrlDetails urlDetails = new UrlDetails();
+ urlDetails.setActualUrl("abcd");
+ urlDetails.setShortUrl("ab");
+ UrlDetails persistedUrlDetails = urlRepository.save(urlDetails);
+
+ //Optional findByShortUrl(String shortUrl);
+ //boolean existsByShortUrl(String shortUrl);
+ Optional byShortUrl = urlRepository.findByShortUrl(persistedUrlDetails.getShortUrl());
+ Assertions.assertFalse(byShortUrl.isEmpty());
+ UrlDetails urlDetailsBySearch = byShortUrl.get();
+ Assertions.assertNotNull(urlDetailsBySearch);
+ Assertions.assertEquals("abcd", urlDetailsBySearch.getActualUrl());
+ }
+
+ @Test
+ void testExistsByShortUrl() {
+ UrlDetails urlDetails = new UrlDetails();
+ urlDetails.setActualUrl("abcd");
+ urlDetails.setShortUrl("ab");
+ UrlDetails persistedUrlDetails = urlRepository.save(urlDetails);
+ Assertions.assertTrue(urlRepository.existsByShortUrl(persistedUrlDetails.getShortUrl()));
+ }
+
+ //for pagination test
+ List getUrlDetails() {
+ //set data
+ UrlDetails urlDetails1 = new UrlDetails();
+ urlDetails1.setActualUrl("http://abcd/123243");
+ urlDetails1.setShortUrl("ab");
+
+ UrlDetails urlDetails2 = new UrlDetails();
+ urlDetails2.setActualUrl("http://pqrst/123243");
+ urlDetails2.setShortUrl("pq");
+
+ UrlDetails urlDetails3 = new UrlDetails();
+ urlDetails3.setActualUrl("http://pqrst12345/123243");
+ urlDetails3.setShortUrl("pq12");
+
+ UrlDetails urlDetails4 = new UrlDetails();
+ urlDetails4.setActualUrl("http://ZZZpqrst12345/123243");
+ urlDetails4.setShortUrl("ZZ12");
+ return List.of(urlDetails1, urlDetails2, urlDetails3, urlDetails4);
+ }
+}
\ No newline at end of file
diff --git a/backend/src/test/java/com/tpx/urlshort/service/UrlServiceIT.java b/backend/src/test/java/com/tpx/urlshort/service/UrlServiceIT.java
new file mode 100644
index 00000000..8000a09a
--- /dev/null
+++ b/backend/src/test/java/com/tpx/urlshort/service/UrlServiceIT.java
@@ -0,0 +1,195 @@
+package com.tpx.urlshort.service;
+
+import com.tpx.urlshort.domain.UrlDetails;
+import com.tpx.urlshort.dto.UrlRequestDTO;
+import com.tpx.urlshort.dto.UrlResponseDTO;
+import com.tpx.urlshort.exception.AliasAlreadyPresentException;
+import com.tpx.urlshort.exception.IllegalParametersException;
+import com.tpx.urlshort.exception.ItemNotFoundException;
+import com.tpx.urlshort.repository.UrlRepository;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+@SpringBootTest
+@ActiveProfiles("test")
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+class UrlServiceIT {
+
+
+
+ @Autowired
+ private UrlService urlService;
+
+ @Autowired
+ private UrlRepository urlRepository;
+
+ @Value("${app.base-url:http://localhost:8080/}")
+ String appBaseUrl;
+
+ @BeforeEach
+ void cleanUp() {
+ urlRepository.deleteAll();
+ }
+
+ @Test
+ void shortenAndPersistURL_shouldSaveAndReturnMappedResponse() {
+ String actualUrl = "https://example.com/some/path";
+ String customAlias = "my-custom-alias";
+ UrlRequestDTO request = new UrlRequestDTO(actualUrl, customAlias);
+ UrlResponseDTO response = urlService.shortenAndPersistURL(request);
+
+ Assertions.assertNotNull(response);
+ Assertions.assertEquals(appBaseUrl+customAlias, response.shortUrl());
+ Assertions.assertEquals(actualUrl, response.actualUrl());
+
+ Optional persisted = urlRepository.findByShortUrl(customAlias);
+ Assertions.assertTrue(persisted.isPresent());
+ Assertions.assertEquals(actualUrl, persisted.get().getActualUrl());
+ }
+
+ @Test
+ void shortenAndPersistURL_whenAliasAlreadyExists_shouldThrowAliasAlreadyPresentException() {
+ UrlDetails existing = new UrlDetails();
+ String actualUrl = "https://already.com";
+ String shortUrl = "dup-alias";
+ String newUrlToShorten = "https://example.com/new";
+ String expectedErrorMessage = "The alias -dup-alias already exist";
+
+ existing.setActualUrl(actualUrl);
+ existing.setShortUrl(shortUrl);
+
+ urlRepository.saveAndFlush(existing);
+ UrlRequestDTO request = new UrlRequestDTO(newUrlToShorten, shortUrl);
+ AliasAlreadyPresentException aliasAlreadyPresentException = Assertions
+ .assertThrows(AliasAlreadyPresentException.class, () -> urlService.shortenAndPersistURL(request));
+ Assertions.assertEquals(expectedErrorMessage, aliasAlreadyPresentException.getMessage());
+ }
+
+ @Test
+ void findByAlias_shouldReturnResponseWhenAliasExists() {
+ UrlDetails existing = new UrlDetails();
+
+ String actualUrl = "https://lookup.com";
+ String shortUrl = "lookup-alias";
+
+ existing.setActualUrl(actualUrl);
+ existing.setShortUrl(shortUrl);
+ urlRepository.saveAndFlush(existing);
+
+ UrlResponseDTO response = urlService.findByAlias(shortUrl);
+
+ Assertions.assertEquals(appBaseUrl+shortUrl, response.shortUrl());
+ Assertions.assertEquals(actualUrl, response.actualUrl());
+ }
+
+ @Test
+ void findByAlias_whenAliasDoesNotExist_shouldThrowItemNotFoundException() {
+ String expectedMessage = "No details found for the alias missing-alias";
+ ItemNotFoundException itemNotFoundException = Assertions.assertThrows(ItemNotFoundException.class,
+ () -> urlService.findByAlias("missing-alias"));
+ Assertions.assertEquals(expectedMessage, itemNotFoundException.getMessage());
+ }
+
+ @Test
+ void findByAlias_whenAliasIsNull_shouldThrowIllegalParametersException() {
+ String expectedMessage = "Invalid parameter alias - null";
+ IllegalParametersException illegalParametersException = Assertions
+ .assertThrows(IllegalParametersException.class, () -> urlService.findByAlias(null));
+ Assertions.assertEquals(expectedMessage, illegalParametersException.getMessage());
+ }
+
+ @Test
+ void delete_shouldRemoveEntryWhenAliasExists() {
+
+ String actualUrl = "https://delete-me.com";
+ String shortUrl = "to-delete";
+
+ UrlDetails existing = new UrlDetails();
+ existing.setActualUrl(actualUrl);
+ existing.setShortUrl(shortUrl);
+ urlRepository.saveAndFlush(existing);
+
+ urlService.delete(shortUrl);
+
+ Assertions.assertFalse(urlRepository.findByShortUrl(shortUrl).isPresent());
+ }
+
+ @Test
+ void delete_whenAliasDoesNotExist_noException() {
+ Assertions.assertThrows(ItemNotFoundException.class,()->urlService.delete("not-here"));
+ }
+
+ @Test
+ void testGetAll() {
+ Pageable pageable = PageRequest.of(0, 3);
+ urlRepository.saveAll(prepareDataForPagination());
+
+ Page page0 = urlService.getAll(pageable);
+ Assertions.assertEquals(3, page0.getSize());
+ Assertions.assertEquals(3, page0.getContent().size());
+
+ // createdAt desc: latest timestamp first; then shortUrl asc when createdAt ties
+ Assertions.assertEquals(appBaseUrl+"xy", page0.getContent().get(0).shortUrl());
+ Assertions.assertEquals(appBaseUrl+"pq", page0.getContent().get(1).shortUrl());
+ Assertions.assertEquals(appBaseUrl+"AB", page0.getContent().get(2).shortUrl());
+
+ UrlDetails urlDetails = new UrlDetails();
+ urlDetails.setActualUrl("http://abcd/123243");
+ urlDetails.setShortUrl("zzz");
+ urlDetails.setCreatedAt(LocalDateTime.of(2026, 8, 26, 12, 0, 0));
+ urlDetails.setUpdatedAt(urlDetails.getCreatedAt());
+
+ // add one more and see that is coming at the beginning
+ urlRepository.save(urlDetails);
+
+ Page page1 = urlService.getAll(pageable);
+ Assertions.assertEquals(3, page1.getSize());
+ Assertions.assertEquals(appBaseUrl+"zzz", page1.getContent().get(0).shortUrl());
+ Assertions.assertEquals(appBaseUrl+"xy", page1.getContent().get(1).shortUrl());
+ Assertions.assertEquals(appBaseUrl+"pq", page1.getContent().get(2).shortUrl());
+
+ }
+
+ private List prepareDataForPagination() {
+ // for pagination test
+ // set data
+ UrlDetails urlDetails1 = new UrlDetails();
+ urlDetails1.setActualUrl("http://abcd/123243");
+ urlDetails1.setShortUrl("AB");
+ urlDetails1.setCreatedAt(LocalDateTime.of(2026, 8, 26, 11, 55, 0));
+ urlDetails1.setUpdatedAt(urlDetails1.getCreatedAt());
+
+ UrlDetails urlDetails2 = new UrlDetails();
+ urlDetails2.setActualUrl("http://pqrst/123243");
+ urlDetails2.setShortUrl("pq");
+ urlDetails2.setCreatedAt(LocalDateTime.of(2026, 8, 26, 11, 58, 0));
+ urlDetails2.setUpdatedAt(urlDetails2.getCreatedAt());
+
+ UrlDetails urlDetails3 = new UrlDetails();
+ urlDetails3.setActualUrl("http://pqrst12345/123243");
+ urlDetails3.setShortUrl("xy");
+ urlDetails3.setCreatedAt(LocalDateTime.of(2026, 8, 26, 11, 59, 0));
+ urlDetails3.setUpdatedAt(urlDetails3.getCreatedAt());
+
+ UrlDetails urlDetails4 = new UrlDetails();
+ urlDetails4.setActualUrl("http://ZZZpqrst12345/123243");
+ urlDetails4.setShortUrl("ZZ12");
+ urlDetails4.setCreatedAt(LocalDateTime.of(2026, 8, 26, 11, 55, 0));
+ urlDetails4.setUpdatedAt(urlDetails4.getCreatedAt());
+
+ return List.of(urlDetails1, urlDetails2, urlDetails3, urlDetails4);
+ }
+}
diff --git a/backend/src/test/java/com/tpx/urlshort/service/UrlServiceTest.java b/backend/src/test/java/com/tpx/urlshort/service/UrlServiceTest.java
new file mode 100644
index 00000000..0d53f2c4
--- /dev/null
+++ b/backend/src/test/java/com/tpx/urlshort/service/UrlServiceTest.java
@@ -0,0 +1,233 @@
+package com.tpx.urlshort.service;
+
+import com.tpx.urlshort.cache.redis.UrlCacheService;
+import com.tpx.urlshort.domain.UrlDetails;
+import com.tpx.urlshort.dto.UrlRequestDTO;
+import com.tpx.urlshort.dto.UrlResponseDTO;
+import com.tpx.urlshort.exception.AliasAlreadyPresentException;
+import com.tpx.urlshort.exception.IllegalParametersException;
+import com.tpx.urlshort.exception.ItemNotFoundException;
+import com.tpx.urlshort.repository.UrlRepository;
+import com.tpx.urlshort.service.alias.AliasResolver;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.dao.DataIntegrityViolationException;
+
+import java.util.Optional;
+
+class UrlServiceTest {
+
+ @Test
+ void testShortenAndPersists() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ String finalAlias = "my-final-alias";
+ String expectedShortUrl = "http://localhost:8080/my-final-alias";
+
+ UrlRequestDTO urlRequestDTO = new UrlRequestDTO("my-full-url", "alias");
+ UrlDetails savedUrlDetails = new UrlDetails(1L, "my-full-url", finalAlias);
+
+ // mock handling
+ Mockito.when(aliasResolverMock.resolveAndGenerate(Mockito.any(UrlRequestDTO.class))).thenReturn(finalAlias);
+ Mockito.when(urlRepositoryMock.saveAndFlush(Mockito.any(UrlDetails.class))).thenReturn(savedUrlDetails);
+
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+ UrlResponseDTO urlResponseDTO = urlService.shortenAndPersistURL(urlRequestDTO);
+ Mockito.verify(urlCacheServiceMock).put(finalAlias, "my-full-url");
+ Assertions.assertEquals(expectedShortUrl, urlResponseDTO.shortUrl());
+ }
+
+ @Test
+ void testShortenAndPersists_for_exception() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ String finalAlias = "my-final-alias";
+ String exceptionMessage = "The alias -my-final-alias already exist";
+ UrlRequestDTO urlRequestDTO = new UrlRequestDTO("my-full-url", "alias");
+ // mock handling
+ Mockito.when(aliasResolverMock.resolveAndGenerate(Mockito.any(UrlRequestDTO.class))).thenReturn(finalAlias);
+ Mockito.when(urlRepositoryMock.saveAndFlush(Mockito.any(UrlDetails.class)))
+ .thenThrow(new DataIntegrityViolationException("Already present"));
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+ AliasAlreadyPresentException aliasAlreadyPresentException = Assertions.assertThrows(
+ AliasAlreadyPresentException.class,
+ () -> urlService.shortenAndPersistURL(urlRequestDTO));
+ Assertions.assertEquals(exceptionMessage, aliasAlreadyPresentException.getMessage());
+
+ }
+
+ @Test
+ void testFindByAlias() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ // Implement the second API
+ String finalAlias = "my-final-alias";
+ String actualUrl = "my-full-url";
+ UrlDetails savedUrlDetails = new UrlDetails(1L, "my-full-url", finalAlias);
+ Optional savedUrlDetailsOptional = Optional.of(savedUrlDetails);
+
+ // handle mock
+ Mockito.when(urlRepositoryMock.findByShortUrl(finalAlias)).thenReturn(savedUrlDetailsOptional);
+
+ Mockito.when(urlCacheServiceMock.get(finalAlias)).thenReturn(Optional.empty());
+
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+ UrlResponseDTO byAlias = urlService.findByAlias(finalAlias);
+ Mockito.verify(urlCacheServiceMock).put(finalAlias, actualUrl);
+ Assertions.assertEquals(actualUrl, byAlias.actualUrl());
+ }
+
+ @Test
+ void testFindByAlias_input_null() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ String finalAlias = null;
+ String expectedErrorMessage = "Invalid parameter alias - null";
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+ IllegalParametersException illegalParametersException = Assertions.assertThrows(
+ IllegalParametersException.class,
+ () -> urlService.findByAlias(finalAlias));
+ Assertions.assertEquals(expectedErrorMessage, illegalParametersException.getMessage());
+ }
+
+ @Test
+ void testFindByAlias_return_empty() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ // Implement the second API
+ String finalAlias = "my-final-alias";
+ String expectedErrorMessage = "No details found for the alias my-final-alias";
+ Optional savedUrlDetailsOptional = Optional.empty();
+
+ // handle mock
+ Mockito.when(urlRepositoryMock.findByShortUrl(finalAlias)).thenReturn(savedUrlDetailsOptional);
+ Mockito.when(urlCacheServiceMock.get(finalAlias)).thenReturn(Optional.empty());
+
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+ ItemNotFoundException itemNotFoundException = Assertions.assertThrows(ItemNotFoundException.class,
+ () -> urlService.findByAlias(finalAlias));
+
+ Assertions.assertEquals(expectedErrorMessage, itemNotFoundException.getMessage());
+ }
+
+ @Test
+ void testDelete_aliasExists() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ String alias = "my-final-alias";
+ UrlDetails savedUrlDetails = new UrlDetails(1L, "my-full-url", alias);
+
+ Mockito.when(urlRepositoryMock.findByShortUrl(alias)).thenReturn(Optional.of(savedUrlDetails));
+
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+ urlService.delete(alias);
+
+ Mockito.verify(urlRepositoryMock).deleteById(1L);
+ Mockito.verify(urlCacheServiceMock).evict(alias);
+ }
+
+ @Test
+ void testFindByAlias_whenCacheReadFails_shouldStillUseDatabase() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ String finalAlias = "my-final-alias";
+ String actualUrl = "my-full-url";
+ UrlDetails savedUrlDetails = new UrlDetails(1L, actualUrl, finalAlias);
+
+ Mockito.when(urlCacheServiceMock.get(finalAlias)).thenThrow(new RuntimeException("Redis down"));
+ Mockito.when(urlRepositoryMock.findByShortUrl(finalAlias)).thenReturn(Optional.of(savedUrlDetails));
+
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+
+ UrlResponseDTO byAlias = urlService.findByAlias(finalAlias);
+
+ Assertions.assertEquals(actualUrl, byAlias.actualUrl());
+ Mockito.verify(urlCacheServiceMock).put(finalAlias, actualUrl);
+ }
+
+ @Test
+ void testShortenAndPersist_whenCacheWriteFails_shouldStillPersist() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ String finalAlias = "my-final-alias";
+ String actualUrl = "my-full-url";
+ UrlRequestDTO urlRequestDTO = new UrlRequestDTO(actualUrl, "alias");
+ UrlDetails savedUrlDetails = new UrlDetails(1L, actualUrl, finalAlias);
+
+ Mockito.when(aliasResolverMock.resolveAndGenerate(Mockito.any(UrlRequestDTO.class))).thenReturn(finalAlias);
+ Mockito.when(urlRepositoryMock.saveAndFlush(Mockito.any(UrlDetails.class))).thenReturn(savedUrlDetails);
+ Mockito.doThrow(new RuntimeException("Redis down")).when(urlCacheServiceMock).put(finalAlias, actualUrl);
+
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+
+ UrlResponseDTO result = urlService.shortenAndPersistURL(urlRequestDTO);
+
+ Assertions.assertEquals("http://localhost:8080/my-final-alias", result.shortUrl());
+ Assertions.assertEquals(actualUrl, result.actualUrl());
+ }
+
+ @Test
+ void testDelete_aliasMissing_shouldThrowItemNotFoundException() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ String alias = "missing-alias";
+ String expectedErrorMessage = "No details found for the alias missing-alias";
+
+ Mockito.when(urlRepositoryMock.findByShortUrl(alias)).thenReturn(Optional.empty());
+
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+
+ ItemNotFoundException itemNotFoundException = Assertions.assertThrows(ItemNotFoundException.class,
+ () -> urlService.delete(alias));
+ Assertions.assertEquals(expectedErrorMessage, itemNotFoundException.getMessage());
+ Mockito.verify(urlRepositoryMock, Mockito.never()).deleteById(Mockito.anyLong());
+ }
+
+ @Test
+ void testDelete_aliasNull_shouldThrowIllegalParametersException() {
+ UrlRepository urlRepositoryMock = Mockito.mock(UrlRepository.class);
+ AliasResolver aliasResolverMock = Mockito.mock(AliasResolver.class);
+ UrlCacheService urlCacheServiceMock = Mockito.mock(UrlCacheService.class);
+
+ String expectedErrorMessage = "Invalid parameter alias - null";
+
+ UrlService urlService = new UrlService(urlRepositoryMock, aliasResolverMock, urlCacheServiceMock);
+ urlService.appBaseUrl = "http://localhost:8080/";
+
+ IllegalParametersException illegalParametersException = Assertions.assertThrows(
+ IllegalParametersException.class,
+ () -> urlService.delete(null));
+
+ Assertions.assertEquals(expectedErrorMessage, illegalParametersException.getMessage());
+
+ }
+
+}
\ No newline at end of file
diff --git a/backend/src/test/java/com/tpx/urlshort/service/alias/AliasResolverTest.java b/backend/src/test/java/com/tpx/urlshort/service/alias/AliasResolverTest.java
new file mode 100644
index 00000000..a59b1ac1
--- /dev/null
+++ b/backend/src/test/java/com/tpx/urlshort/service/alias/AliasResolverTest.java
@@ -0,0 +1,73 @@
+package com.tpx.urlshort.service.alias;
+
+import com.tpx.urlshort.dto.UrlRequestDTO;
+import com.tpx.urlshort.exception.ConfigMissingException;
+import com.tpx.urlshort.exception.IllegalParametersException;
+import com.tpx.urlshort.service.alias.snowflake.SnowflakeIdGenerator;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+class AliasResolverTest {
+
+ @Test
+ void testCustomGenerator() {
+ AliasGenerator customGenerator = new CustomAliasGenerator();
+ SnowflakeIdGenerator snowflakeIdGeneratorMock = Mockito.mock(SnowflakeIdGenerator.class);
+ Base62Encoder base62EncoderMock = Mockito.mock(Base62Encoder.class);
+ AliasGenerator snowFlakeIdGenerator = new SnowflakeAliasGenerator(snowflakeIdGeneratorMock, base62EncoderMock);
+ AliasResolver aliasResolver = new AliasResolver(List.of(customGenerator, snowFlakeIdGenerator));
+ UrlRequestDTO urlRequestDTO = new UrlRequestDTO("my-long-url", "mlu");
+ String finalAlias = aliasResolver.resolveAndGenerate(urlRequestDTO);
+ Assertions.assertEquals("mlu", finalAlias);
+ }
+
+ @Test
+ void testCustomGenerator_when_both_missing() {
+ AliasResolver aliasResolver = new AliasResolver(List.of());
+ UrlRequestDTO urlRequestDTO = new UrlRequestDTO("my-long-url", "mlu");
+ String expectedExceptionMessage = "No suitable alias generator found!";
+ ConfigMissingException configMissingException = Assertions.assertThrows(ConfigMissingException.class, () -> {
+ aliasResolver.resolveAndGenerate(urlRequestDTO);
+ });
+ Assertions.assertEquals(expectedExceptionMessage, configMissingException.getMessage());
+ }
+
+ @Test
+ void testCustomGenerator_actual_url_isNull() {
+ AliasGenerator customGenerator = new CustomAliasGenerator();
+ SnowflakeIdGenerator snowflakeIdGeneratorMock = Mockito.mock(SnowflakeIdGenerator.class);
+ Base62Encoder base62EncoderMock = Mockito.mock(Base62Encoder.class);
+ AliasGenerator snowFlakeIdGenerator = new SnowflakeAliasGenerator(snowflakeIdGeneratorMock, base62EncoderMock);
+ AliasResolver aliasResolver = new AliasResolver(List.of(customGenerator, snowFlakeIdGenerator));
+ UrlRequestDTO urlRequestDTO = new UrlRequestDTO(null, null);
+ String expectedExceptionMessage = "Invalid parameters , request or actual url cannot be null or empty";
+ IllegalParametersException illegalParametersException = Assertions
+ .assertThrows(IllegalParametersException.class, () -> {
+ aliasResolver.resolveAndGenerate(urlRequestDTO);
+ });
+ Assertions.assertEquals(expectedExceptionMessage, illegalParametersException.getMessage());
+ }
+
+ @Test
+ void testCustomGenerator_alias_isEmpty() {
+ SnowflakeIdGenerator snowflakeIdGeneratorMock = Mockito.mock(SnowflakeIdGenerator.class);
+ Base62Encoder base62EncoderMock = Mockito.mock(Base62Encoder.class);
+ AliasGenerator customGenerator = new CustomAliasGenerator();
+ AliasGenerator snowFlakeIdGenerator = new SnowflakeAliasGenerator(snowflakeIdGeneratorMock, base62EncoderMock);
+ long uniqueNumber = 564654654L;
+ String expectedAlias = "helowld";
+ // handle the mock for the snowFlake and the base62 encoder
+ Mockito.when(snowflakeIdGeneratorMock.getNextId()).thenReturn(uniqueNumber);
+ Mockito.when(base62EncoderMock.encode(uniqueNumber)).thenReturn(expectedAlias);
+
+ AliasResolver aliasResolver = new AliasResolver(List.of(customGenerator, snowFlakeIdGenerator));
+ UrlRequestDTO urlRequestDTO = new UrlRequestDTO("my-long-url", null);
+
+ String finalAlias = aliasResolver.resolveAndGenerate(urlRequestDTO);
+ Assertions.assertEquals(expectedAlias, finalAlias);
+ }
+
+}
\ No newline at end of file
diff --git a/backend/src/test/java/com/tpx/urlshort/service/alias/Base62EncoderTest.java b/backend/src/test/java/com/tpx/urlshort/service/alias/Base62EncoderTest.java
new file mode 100644
index 00000000..b81b24f7
--- /dev/null
+++ b/backend/src/test/java/com/tpx/urlshort/service/alias/Base62EncoderTest.java
@@ -0,0 +1,53 @@
+package com.tpx.urlshort.service.alias;
+
+import com.tpx.urlshort.exception.IllegalParametersException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class Base62EncoderTest {
+
+ @Test
+ void testEncode() {
+ Base62Encoder base62Encoder = new Base62Encoder();
+ String encodeFirst = base62Encoder.encode(123);
+ String expectedValue = "aaaaa1Z";
+ Assertions.assertEquals(expectedValue, encodeFirst);
+
+ // This is expected to be deterministic
+ String encodeSecond = base62Encoder.encode(123);
+ Assertions.assertEquals(encodeFirst, encodeSecond);
+
+ // its length must be 7
+ Assertions.assertEquals(7, encodeFirst.length());
+ Assertions.assertEquals(7, encodeSecond.length());
+ }
+
+ @Test
+ void testEncodeWhenNumber_negative() {
+ Base62Encoder base62Encoder = new Base62Encoder();
+ String expectedError = "Invalid input too less to consider -123";
+ IllegalParametersException illegalParametersException = Assertions
+ .assertThrows(IllegalParametersException.class, () -> base62Encoder.encode(-123));
+ Assertions.assertEquals(expectedError, illegalParametersException.getMessage());
+ }
+
+ @Test
+ void testEncode_more_than_upper_threshold() {
+ Base62Encoder base62Encoder = new Base62Encoder();
+ long input = 3521614606208L;
+ String expectedError = "Value 3521614606208 exceeds maximum capacity for 7 chars ( 3521614606207 )";
+ IllegalParametersException illegalParametersException = Assertions
+ .assertThrows(IllegalParametersException.class, () -> base62Encoder.encode(input));
+ Assertions.assertEquals(expectedError, illegalParametersException.getMessage());
+ }
+
+ @Test
+ void testEncode_largeValue_shouldNotOverflowIndex() {
+ Base62Encoder base62Encoder = new Base62Encoder();
+ long input = 3_000_000_000L;
+
+ String encoded = Assertions.assertDoesNotThrow(() -> base62Encoder.encode(input));
+
+ Assertions.assertEquals(7, encoded.length());
+ }
+}
\ No newline at end of file
diff --git a/backend/src/test/java/com/tpx/urlshort/service/alias/snowflake/SnowflakeIdGeneratorTest.java b/backend/src/test/java/com/tpx/urlshort/service/alias/snowflake/SnowflakeIdGeneratorTest.java
new file mode 100644
index 00000000..91e59149
--- /dev/null
+++ b/backend/src/test/java/com/tpx/urlshort/service/alias/snowflake/SnowflakeIdGeneratorTest.java
@@ -0,0 +1,37 @@
+package com.tpx.urlshort.service.alias.snowflake;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+
+class SnowflakeIdGeneratorTest {
+
+ private static final long EPOCH_SECONDS = 1767225600L;
+
+ @Test
+ void testGetCurrentTimestamp() {
+ SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1L, 1L, EPOCH_SECONDS);
+ long currentTimestamp = snowflakeIdGenerator.getCurrentTimestamp();
+ Assertions.assertTrue(currentTimestamp > 0L);
+ }
+
+ @Test
+ void testWaitAndGetNextTimestamp() {
+ SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1L, 1L, EPOCH_SECONDS);
+ long epochCurrentSecond = Instant.now().getEpochSecond();
+ long newTimestamp = snowflakeIdGenerator.waitAndGetNextTimestamp(epochCurrentSecond);
+ Assertions.assertTrue(epochCurrentSecond < newTimestamp);
+ }
+
+ @Test
+ void testGetNextId(){
+ SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1L, 1L, EPOCH_SECONDS);
+ long first = snowflakeIdGenerator.getNextId();
+ long second = snowflakeIdGenerator.getNextId();
+ System.out.println(first);
+ System.out.println(second);
+ Assertions.assertNotEquals(first, second);
+ }
+
+}
\ No newline at end of file
diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml
new file mode 100644
index 00000000..66ec27e5
--- /dev/null
+++ b/backend/src/test/resources/application-test.yml
@@ -0,0 +1,25 @@
+spring:
+ jpa:
+ show-sql: true
+ hibernate:
+ ddl-auto: 'create-drop'
+ database-platform: org.hibernate.community.dialect.SQLiteDialect
+ datasource:
+ driver-class-name: org.sqlite.JDBC
+ url: 'jdbc:sqlite::memory:'
+ data:
+ redis:
+ host: localhost
+ port: 6379
+ timeout: 2000ms
+app:
+ base-url: http://localhost:8080/
+ cors:
+ allowed-origins:
+ - http://localhost:5173
+ - http://localhost:5174
+
+snowflake:
+ worker-id: 2
+ datacenter-id: 2
+ epoch-reference: 1767225600
diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml
new file mode 100644
index 00000000..41b7cd9d
--- /dev/null
+++ b/backend/src/test/resources/application.yml
@@ -0,0 +1,19 @@
+spring:
+ profiles:
+ active:
+ - test
+
+
+app:
+ cache:
+ ttl-seconds: 600
+ cors:
+ allowed-methods:
+ - GET
+ - POST
+ - PUT
+ - DELETE
+ - OPTIONS
+ allowed-headers:
+ - "*"
+ allow-credentials: true
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..90563393
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,54 @@
+services:
+ # 1. Database/Cache Layer
+ redis:
+ image: redis:7-alpine
+ container_name: spring-redis-cache
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+ networks:
+ - app-network
+
+ # 2. Backend Layer (Spring Boot)
+ backend:
+ build:
+ context: ./backend # Path to your Spring Boot project folder
+ dockerfile: Dockerfile
+ container_name: spring-boot-api
+ ports:
+ - "8585:8585"
+ environment:
+ - SPRING_DATA_REDIS_HOST=redis
+ - SPRING_DATA_REDIS_PORT=6379
+ - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-http://localhost}
+ - APP_BASE_URL=${APP_BASE_URL:-http://localhost:8585/api/v1/}
+ depends_on:
+ - redis
+ networks:
+ - app-network
+
+ # 3. Frontend Layer (React)
+ frontend:
+ build:
+ context: ./frontend # Path to your React project folder
+ dockerfile: Dockerfile
+ container_name: react-ui
+ ports:
+ - "80:80"
+ environment:
+ - VITE_API_PROTOCOL=${VITE_API_PROTOCOL:-http}
+ - VITE_API_HOST=${VITE_API_HOST:-localhost}
+ - VITE_API_PORT=${VITE_API_PORT:-8585}
+ - VITE_API_BASE_URL=${VITE_API_BASE_URL:-}
+ depends_on:
+ - backend
+ networks:
+ - app-network
+
+volumes:
+ redis_data:
+
+networks:
+ app-network:
+ driver: bridge
diff --git a/frontend/.env.example b/frontend/.env.example
new file mode 100644
index 00000000..73712cbd
--- /dev/null
+++ b/frontend/.env.example
@@ -0,0 +1,7 @@
+# Option 1: split protocol/host/port
+VITE_API_PROTOCOL=http
+VITE_API_HOST=localhost
+VITE_API_PORT=8585
+
+# Option 2 (overrides the values above): full API base URL
+# VITE_API_BASE_URL=http://localhost:8585
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json
new file mode 100644
index 00000000..6fa991da
--- /dev/null
+++ b/frontend/.oxlintrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 00000000..99a57542
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,18 @@
+# Stage 1: Build the React application
+FROM node:20-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY . .
+RUN npm run build
+
+# Stage 2: Serve the production static files using Nginx
+FROM nginx:stable-alpine
+# Copy the compiled static assets from Stage 1 to Nginx public folder
+COPY --from=build /app/dist /usr/share/nginx/html
+# Copy custom nginx configuration to handle routing if needed
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY docker-entrypoint-runtime-config.sh /docker-entrypoint.d/40-runtime-config.sh
+RUN chmod +x /docker-entrypoint.d/40-runtime-config.sh
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 00000000..18e05e6e
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,80 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
+
+## API configuration
+
+Frontend API endpoint settings are read from Vite environment variables so backend host/port changes do not require code changes.
+
+Create a `.env` file in the project root with one of the following options:
+
+Option 1: Configure host/port separately
+
+```env
+VITE_API_PROTOCOL=http
+VITE_API_HOST=localhost
+VITE_API_PORT=8585
+```
+
+Option 2: Configure complete base URL directly
+
+```env
+VITE_API_BASE_URL=http://localhost:8585
+```
+
+`VITE_API_BASE_URL` takes precedence when set.
+
+Important: for Vite apps this value is baked into static assets at build time. In Docker, if you change these values, rebuild the image and redeploy the container.
+
+## Docker runtime configuration (no rebuild needed)
+
+This project also supports runtime API configuration in Docker using `config.js` generated at container startup.
+
+After building once, you can change API settings by passing container environment variables:
+
+```bash
+docker run -p 3000:80 \
+ -e VITE_API_PROTOCOL=http \
+ -e VITE_API_HOST=host.docker.internal \
+ -e VITE_API_PORT=8585 \
+ code-exercise-java-frontend
+```
+
+Or use a full base URL override:
+
+```bash
+docker run -p 3000:80 \
+ -e VITE_API_BASE_URL=http://host.docker.internal:8585 \
+ code-exercise-java-frontend
+```
+
+With this runtime mode, container restarts pick up new values without rebuilding the image.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the Oxlint configuration
+
+If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
+
+```json
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "options": {
+ "typeAware": true
+ },
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
+```
+
+See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
diff --git a/frontend/docker-entrypoint-runtime-config.sh b/frontend/docker-entrypoint-runtime-config.sh
new file mode 100644
index 00000000..6c7f0d69
--- /dev/null
+++ b/frontend/docker-entrypoint-runtime-config.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+set -eu
+
+TEMPLATE_PATH="/usr/share/nginx/html/config.js.template"
+OUTPUT_PATH="/usr/share/nginx/html/config.js"
+
+API_PROTOCOL="${VITE_API_PROTOCOL:-http}"
+API_HOST="${VITE_API_HOST:-localhost}"
+API_PORT="${VITE_API_PORT:-8585}"
+API_BASE_URL="${VITE_API_BASE_URL:-}"
+
+escape_for_sed() {
+ printf '%s' "$1" | sed 's/[&|]/\\&/g'
+}
+
+if [ -f "$TEMPLATE_PATH" ]; then
+ sed \
+ -e "s|__API_PROTOCOL__|$(escape_for_sed "$API_PROTOCOL")|g" \
+ -e "s|__API_HOST__|$(escape_for_sed "$API_HOST")|g" \
+ -e "s|__API_PORT__|$(escape_for_sed "$API_PORT")|g" \
+ -e "s|__API_BASE_URL__|$(escape_for_sed "$API_BASE_URL")|g" \
+ "$TEMPLATE_PATH" > "$OUTPUT_PATH"
+fi
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 00000000..911e98ea
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ frontend
+
+
+
+
+
+
+
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
new file mode 100644
index 00000000..2b87a60d
--- /dev/null
+++ b/frontend/nginx.conf
@@ -0,0 +1,14 @@
+server {
+ listen 80;
+
+ location = /config.js {
+ root /usr/share/nginx/html;
+ add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
+ }
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ try_files $uri $uri/ /index.html;
+ }
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 00000000..8ca8e5bb
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,4175 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.3",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^24.13.3",
+ "@types/react": "^19.2.18",
+ "@types/react-dom": "^19.2.4",
+ "@vitejs/plugin-react": "^6.1.0",
+ "jsdom": "^26.1.0",
+ "oxlint": "^1.79.0",
+ "tailwindcss": "^4.3.3",
+ "typescript": "~6.0.2",
+ "vite": "^8.2.2",
+ "vitest": "^3.2.4"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^22.20 || ^24.12 || >=25"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.147.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz",
+ "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm-eabi": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.80.0.tgz",
+ "integrity": "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm64": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.80.0.tgz",
+ "integrity": "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-arm64": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.80.0.tgz",
+ "integrity": "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-x64": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.80.0.tgz",
+ "integrity": "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-freebsd-x64": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.80.0.tgz",
+ "integrity": "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.80.0.tgz",
+ "integrity": "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-musleabihf": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.80.0.tgz",
+ "integrity": "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.80.0.tgz",
+ "integrity": "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-musl": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.80.0.tgz",
+ "integrity": "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-ppc64-gnu": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.80.0.tgz",
+ "integrity": "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-gnu": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.80.0.tgz",
+ "integrity": "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-musl": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.80.0.tgz",
+ "integrity": "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-s390x-gnu": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.80.0.tgz",
+ "integrity": "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-gnu": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.80.0.tgz",
+ "integrity": "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-musl": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.80.0.tgz",
+ "integrity": "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-openharmony-arm64": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.80.0.tgz",
+ "integrity": "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.80.0.tgz",
+ "integrity": "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-ia32-msvc": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.80.0.tgz",
+ "integrity": "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-x64-msvc": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.80.0.tgz",
+ "integrity": "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm-eabi": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz",
+ "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz",
+ "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz",
+ "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz",
+ "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz",
+ "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz",
+ "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz",
+ "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz",
+ "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz",
+ "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz",
+ "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz",
+ "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz",
+ "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz",
+ "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz",
+ "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz",
+ "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz",
+ "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz",
+ "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz",
+ "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz",
+ "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz",
+ "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz",
+ "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz",
+ "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz",
+ "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz",
+ "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz",
+ "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz",
+ "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz",
+ "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz",
+ "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz",
+ "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz",
+ "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz",
+ "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz",
+ "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz",
+ "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz",
+ "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz",
+ "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz",
+ "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz",
+ "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz",
+ "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz",
+ "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz",
+ "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
+ "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.24.1",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
+ "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-x64": "4.3.3",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.3",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.3",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.3",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
+ "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
+ "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
+ "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
+ "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
+ "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
+ "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
+ "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
+ "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
+ "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
+ "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
+ "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
+ "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
+ "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.3",
+ "@tailwindcss/oxide": "4.3.3",
+ "tailwindcss": "4.3.3"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
+ "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.6",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz",
+ "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
+ "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz",
+ "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz",
+ "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxc-transform-react": "^0.145.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "oxc-transform-react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
+ "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
+ "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
+ "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.7",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
+ "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
+ "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
+ "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.24.5",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz",
+ "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.27",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz",
+ "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/oxlint": {
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.80.0.tgz",
+ "integrity": "sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "oxlint": "bin/oxlint"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxlint/binding-android-arm-eabi": "1.80.0",
+ "@oxlint/binding-android-arm64": "1.80.0",
+ "@oxlint/binding-darwin-arm64": "1.80.0",
+ "@oxlint/binding-darwin-x64": "1.80.0",
+ "@oxlint/binding-freebsd-x64": "1.80.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.80.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.80.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.80.0",
+ "@oxlint/binding-linux-arm64-musl": "1.80.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.80.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.80.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.80.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.80.0",
+ "@oxlint/binding-linux-x64-gnu": "1.80.0",
+ "@oxlint/binding-linux-x64-musl": "1.80.0",
+ "@oxlint/binding-openharmony-arm64": "1.80.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.80.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.80.0",
+ "@oxlint/binding-win32-x64-msvc": "1.80.0"
+ },
+ "peerDependencies": {
+ "oxlint-tsgolint": ">=7.0.2001",
+ "vite-plus": "*"
+ },
+ "peerDependenciesMeta": {
+ "oxlint-tsgolint": {
+ "optional": true
+ },
+ "vite-plus": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.17",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
+ "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.8"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz",
+ "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.147.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm-eabi": "1.2.6",
+ "@rolldown/binding-android-arm64": "1.2.6",
+ "@rolldown/binding-darwin-arm64": "1.2.6",
+ "@rolldown/binding-darwin-x64": "1.2.6",
+ "@rolldown/binding-freebsd-x64": "1.2.6",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.6",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.6",
+ "@rolldown/binding-linux-arm64-musl": "1.2.6",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.6",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.6",
+ "@rolldown/binding-linux-x64-gnu": "1.2.6",
+ "@rolldown/binding-linux-x64-musl": "1.2.6",
+ "@rolldown/binding-openharmony-arm64": "1.2.6",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.6",
+ "@rolldown/binding-win32-x64-msvc": "1.2.6"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.63.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz",
+ "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+ "@rollup/rollup-android-arm-eabi": "4.63.1",
+ "@rollup/rollup-android-arm64": "4.63.1",
+ "@rollup/rollup-darwin-arm64": "4.63.1",
+ "@rollup/rollup-darwin-x64": "4.63.1",
+ "@rollup/rollup-freebsd-arm64": "4.63.1",
+ "@rollup/rollup-freebsd-x64": "4.63.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.63.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.63.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.63.1",
+ "@rollup/rollup-linux-arm64-musl": "4.63.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.63.1",
+ "@rollup/rollup-linux-loong64-musl": "4.63.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.63.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.63.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.63.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.63.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.63.1",
+ "@rollup/rollup-linux-x64-gnu": "4.63.1",
+ "@rollup/rollup-linux-x64-musl": "4.63.1",
+ "@rollup/rollup-openbsd-x64": "4.63.1",
+ "@rollup/rollup-openharmony-arm64": "4.63.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.63.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.63.1",
+ "@rollup/rollup-win32-x64-gnu": "4.63.1",
+ "@rollup/rollup-win32-x64-msvc": "4.63.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
+ "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
+ "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.26",
+ "rolldown": "~1.2.4",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.4.0 || ^0.5.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite-node/node_modules/vite": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
+ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.7",
+ "@vitest/mocker": "3.2.7",
+ "@vitest/pretty-format": "^3.2.7",
+ "@vitest/runner": "3.2.7",
+ "@vitest/snapshot": "3.2.7",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.7",
+ "@vitest/ui": "3.2.7",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/@vitest/mocker": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
+ "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.7",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/vite": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 00000000..28a6053c
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "oxlint",
+ "preview": "vite preview",
+ "test": "vitest",
+ "test:run": "vitest run"
+ },
+ "dependencies": {
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.3",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^24.13.3",
+ "@types/react": "^19.2.18",
+ "@types/react-dom": "^19.2.4",
+ "@vitejs/plugin-react": "^6.1.0",
+ "jsdom": "^26.1.0",
+ "oxlint": "^1.79.0",
+ "tailwindcss": "^4.3.3",
+ "typescript": "~6.0.2",
+ "vite": "^8.2.2",
+ "vitest": "^3.2.4"
+ }
+}
diff --git a/frontend/public/config.js b/frontend/public/config.js
new file mode 100644
index 00000000..b39992f1
--- /dev/null
+++ b/frontend/public/config.js
@@ -0,0 +1,6 @@
+window.__APP_CONFIG__ = {
+ API_PROTOCOL: 'http',
+ API_HOST: 'localhost',
+ API_PORT: '8585',
+ API_BASE_URL: '',
+};
diff --git a/frontend/public/config.js.template b/frontend/public/config.js.template
new file mode 100644
index 00000000..364df2e8
--- /dev/null
+++ b/frontend/public/config.js.template
@@ -0,0 +1,6 @@
+window.__APP_CONFIG__ = {
+ API_PROTOCOL: "__API_PROTOCOL__",
+ API_HOST: "__API_HOST__",
+ API_PORT: "__API_PORT__",
+ API_BASE_URL: "__API_BASE_URL__"
+};
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 00000000..e69de29b
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 00000000..7e779386
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,53 @@
+import React from 'react'
+import './App.css'
+import URLShortner from './components/ULShortner'
+import UrlList from './components/UrlList'
+
+function App() {
+ const [urlListRefreshKey, setUrlListRefreshKey] = React.useState(0);
+ const [activeView, setActiveView] = React.useState<'shortener' | 'list'>('shortener');
+
+ const handleUrlCreated = () => {
+ setUrlListRefreshKey((prev) => prev + 1);
+ };
+
+ return (
+
+ {activeView === 'shortener' ? (
+ <>
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+ >
+ )}
+
+ )
+}
+
+export default App
diff --git a/frontend/src/api/apiCaller.integration.test.ts b/frontend/src/api/apiCaller.integration.test.ts
new file mode 100644
index 00000000..183baab5
--- /dev/null
+++ b/frontend/src/api/apiCaller.integration.test.ts
@@ -0,0 +1,84 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { shortenUrl, deleteUrl, ApiError } from './apiCaller';
+
+describe('apiCaller integration', () => {
+ const originalFetch = global.fetch;
+
+ beforeEach(() => {
+ global.fetch = vi.fn();
+ });
+
+ afterEach(() => {
+ global.fetch = originalFetch;
+ vi.restoreAllMocks();
+ });
+
+ it('returns URL response for successful shortenUrl call', async () => {
+ vi.mocked(global.fetch).mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ customAlias: 'abc',
+ fullUrl: 'https://example.com',
+ shortUrl: 'http://localhost:8081/abc',
+ }),
+ {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ },
+ ),
+ );
+
+ const response = await shortenUrl({ fullUrl: 'https://example.com', customAlias: 'abc' });
+
+ expect(response.shortUrl).toBe('http://localhost:8081/abc');
+ });
+
+ it('throws ApiError with ApiErrorResponse details for JSON error body', async () => {
+ vi.mocked(global.fetch).mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ status: 409,
+ error: 'Conflict',
+ message: 'Alias already exists',
+ timestamp: '2026-08-31T00:00:00.000Z',
+ }),
+ {
+ status: 409,
+ statusText: 'Conflict',
+ headers: { 'Content-Type': 'application/json' },
+ },
+ ),
+ );
+
+ await expect(shortenUrl({ fullUrl: 'https://example.com', customAlias: 'abc' })).rejects.toMatchObject({
+ name: 'ApiError',
+ details: {
+ status: 409,
+ error: 'Conflict',
+ message: 'Alias already exists',
+ },
+ });
+ });
+
+ it('throws fallback ApiError when deleteUrl receives non-JSON error response', async () => {
+ vi.mocked(global.fetch).mockResolvedValue(
+ new Response('Delete failed on server', {
+ status: 500,
+ statusText: 'Internal Server Error',
+ headers: { 'Content-Type': 'text/plain' },
+ }),
+ );
+
+ try {
+ await deleteUrl('abc');
+ throw new Error('Expected deleteUrl to throw');
+ } catch (error) {
+ expect(error).toBeInstanceOf(ApiError);
+ const apiError = error as ApiError;
+ expect(apiError.details.status).toBe(500);
+ expect(apiError.details.error).toBe('Internal Server Error');
+ expect(apiError.details.message).toBe('Delete failed on server');
+ expect(apiError.details.timestamp.length).toBeGreaterThan(0);
+ }
+ });
+});
diff --git a/frontend/src/api/apiCaller.ts b/frontend/src/api/apiCaller.ts
new file mode 100644
index 00000000..da59ab95
--- /dev/null
+++ b/frontend/src/api/apiCaller.ts
@@ -0,0 +1,116 @@
+import type { ApiErrorResponse } from '../types/ApiErrorResponse';
+import type { UrlRequest, UrlResponse, UrlItem } from '../types/url';
+import type { UrlRecord } from '../types/UrlRecord';
+import { API_BASE_URL } from './config';
+
+export type PageResponse = {
+ content: T[];
+ totalPages: number;
+ totalElements: number;
+ number: number;
+ size: number;
+ first: boolean;
+ last: boolean;
+ numberOfElements: number;
+};
+
+export class ApiError extends Error {
+ details: ApiErrorResponse;
+
+ constructor(details: ApiErrorResponse) {
+ super(details.message || details.error || 'Request failed');
+ this.name = 'ApiError';
+ this.details = details;
+ }
+}
+
+function isApiErrorResponse(value: unknown): value is ApiErrorResponse {
+ if (!value || typeof value !== 'object') {
+ return false;
+ }
+
+ const obj = value as Record;
+ return (
+ typeof obj.status === 'number' &&
+ typeof obj.error === 'string' &&
+ typeof obj.message === 'string' &&
+ typeof obj.timestamp === 'string'
+ );
+}
+
+async function parseApiError(response: Response, fallbackMessage: string): Promise {
+ const text = await response.text().catch(() => '');
+
+ if (text) {
+ try {
+ const body = JSON.parse(text) as unknown;
+ if (isApiErrorResponse(body)) {
+ return body;
+ }
+ } catch {
+ // Fall through to plain text error body handling.
+ }
+ }
+
+ return {
+ status: response.status,
+ error: response.statusText || 'Request failed',
+ message: text || fallbackMessage,
+ timestamp: new Date().toISOString(),
+ };
+}
+
+async function handleResponse(response: Response, fallbackMessage = 'Request failed'): Promise {
+ if (!response.ok) {
+ const errorBody = await parseApiError(response, fallbackMessage);
+ throw new ApiError(errorBody);
+ }
+
+ return response.json() as Promise;
+}
+
+export async function shortenUrl(payload: UrlRequest): Promise {
+ const response = await fetch(`${API_BASE_URL}/api/v1/shorten`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+ return handleResponse(response);
+}
+
+export async function getAllUrls(): Promise {
+ const response = await fetch(`${API_BASE_URL}/api/v1/urls`);
+
+ return handleResponse(response);
+}
+
+export async function getUrlsPage(query: {
+ page: number;
+ size: number;
+ alias?: string;
+}): Promise> {
+ const params = new URLSearchParams({
+ page: String(query.page),
+ size: String(query.size),
+ });
+
+ if (query.alias) {
+ params.set('alias', query.alias);
+ }
+
+ const response = await fetch(`${API_BASE_URL}/api/v1/urls?${params.toString()}`);
+ return handleResponse>(response, 'Failed to fetch URLs');
+}
+
+export async function deleteUrl(alias: string): Promise {
+ const response = await fetch(`${API_BASE_URL}/api/v1/${encodeURIComponent(alias)}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ const errorBody = await parseApiError(response, 'Failed to delete URL');
+ throw new ApiError(errorBody);
+ }
+}
diff --git a/frontend/src/api/config.ts b/frontend/src/api/config.ts
new file mode 100644
index 00000000..742437c6
--- /dev/null
+++ b/frontend/src/api/config.ts
@@ -0,0 +1,23 @@
+type RuntimeConfig = {
+ API_PROTOCOL?: string;
+ API_HOST?: string;
+ API_PORT?: string;
+ API_BASE_URL?: string;
+};
+
+function getRuntimeConfig(): RuntimeConfig {
+ const value = (globalThis as { __APP_CONFIG__?: RuntimeConfig }).__APP_CONFIG__;
+ return value || {};
+}
+
+const runtime = getRuntimeConfig();
+
+const protocol = runtime.API_PROTOCOL || import.meta.env.VITE_API_PROTOCOL || 'http';
+const host = runtime.API_HOST || import.meta.env.VITE_API_HOST || 'localhost';
+const port = runtime.API_PORT || import.meta.env.VITE_API_PORT || '8585';
+
+// Runtime API_BASE_URL overrides all; then build-time VITE_API_BASE_URL; then compose from protocol/host/port.
+export const API_BASE_URL =
+ runtime.API_BASE_URL ||
+ import.meta.env.VITE_API_BASE_URL ||
+ `${protocol}://${host}:${port}`;
diff --git a/frontend/src/components/ULShortner.tsx b/frontend/src/components/ULShortner.tsx
new file mode 100644
index 00000000..d47c1df9
--- /dev/null
+++ b/frontend/src/components/ULShortner.tsx
@@ -0,0 +1,242 @@
+import React from 'react';
+import { ApiError, shortenUrl } from '../api/apiCaller';
+import type { ApiErrorResponse } from '../types/ApiErrorResponse';
+import ErrorModal from './error/ErrorModal';
+
+type URLShortnerProps = {
+ onUrlCreated?: () => void;
+};
+
+const initialForm = {
+ fullUrl: '',
+ customAlias: '',
+};
+
+//for checking the validity of the URL
+// as part of input validation before sending the request to the backend
+function isValidUrl(value: string) {
+ try {
+ new URL(value);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+
+function URLShortner({ onUrlCreated }: URLShortnerProps) : React.ReactElement {
+
+ const [form, setForm] = React.useState(initialForm);
+ const [loading, setLoading] = React.useState(false);
+ const [apiError, setApiError] = React.useState(null);
+ const [error, setError] = React.useState('');
+ const [result, setResult] = React.useState('');
+ const [copyMessage, setCopyMessage] = React.useState('');
+
+ const handleChange = (field: 'fullUrl' | 'customAlias', value: string) => {
+ setForm((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ setError('');
+ setApiError(null);
+
+ // get the trimmed URL from the form state
+ const trimmedUrl = form.fullUrl.trim();
+
+ //input validation - BOC
+ //check if the trimmed URL is empty or invalid
+ if (!trimmedUrl) {
+ document.getElementById('fullUrl')?.focus();
+ setError('Original URL is required.');
+ return;
+ }
+
+ // check if the trimmed URL is valid
+ if (!isValidUrl(trimmedUrl)) {
+ setError('Please enter a valid URL.');
+ return;
+ }
+
+ //input validation - EOC
+
+ setLoading(true);
+ setResult('');
+ setCopyMessage('');
+
+ try {
+ console.log('Submitting:', { fullUrl: trimmedUrl, customAlias: form.customAlias.trim() || undefined });
+ const response = await shortenUrl({
+ fullUrl: trimmedUrl,
+ customAlias: form.customAlias.trim() || undefined,
+ });
+
+ if (!response?.shortUrl) {
+ setError('Invalid response from server');
+ return;
+ }
+ setResult(`${response.shortUrl}`);
+ setForm(initialForm);
+ onUrlCreated?.();
+ } catch (err) {
+ if (err instanceof ApiError) {
+ setApiError(err.details);
+ } else {
+ setError(err instanceof Error ? err.message : 'Something went wrong');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const copyToClipboard = async () => {
+ if (!result) return;
+ try {
+ await navigator.clipboard.writeText(result);
+ setCopyMessage('Short URL copied to clipboard.');
+ } catch {
+ setCopyMessage('Unable to copy automatically. Please copy the URL manually.');
+ }
+ };
+
+
+ function handleReset() {
+ setForm(initialForm);
+ setError('');
+ setResult('');
+ setApiError(null);
+ setCopyMessage('');
+ }
+
+ return (
+
+ );
+}
+export default URLShortner;
+
diff --git a/frontend/src/components/ULShortner.unit.test.tsx b/frontend/src/components/ULShortner.unit.test.tsx
new file mode 100644
index 00000000..0f092e0f
--- /dev/null
+++ b/frontend/src/components/ULShortner.unit.test.tsx
@@ -0,0 +1,101 @@
+import React from 'react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import URLShortner from './ULShortner';
+import { ApiError, shortenUrl } from '../api/apiCaller';
+
+vi.mock('../api/apiCaller', async () => {
+ const actual = await vi.importActual('../api/apiCaller');
+ return {
+ ...actual,
+ shortenUrl: vi.fn(),
+ };
+});
+
+describe('URLShortner unit', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('shows validation error when URL is empty', async () => {
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Shorten URL' }));
+
+ const alert = await screen.findByRole('alert');
+ expect(alert.textContent).toContain('Original URL is required.');
+ expect(shortenUrl).not.toHaveBeenCalled();
+ });
+
+ it('shows validation error for invalid URL', async () => {
+ render();
+
+ fireEvent.change(screen.getByLabelText('Enter URL to shorten:'), {
+ target: { value: 'not-a-valid-url' },
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Shorten URL' }));
+
+ const alert = await screen.findByRole('alert');
+ expect(alert.textContent).toContain('Please enter a valid URL.');
+ expect(shortenUrl).not.toHaveBeenCalled();
+ });
+
+ it('renders shortened URL on success', async () => {
+ vi.mocked(shortenUrl).mockResolvedValue({
+ customAlias: 'my-alias',
+ fullUrl: 'https://example.com/path',
+ shortUrl: 'http://localhost:8081/my-alias',
+ });
+
+ render();
+
+ fireEvent.change(screen.getByLabelText('Enter URL to shorten:'), {
+ target: { value: 'https://example.com/path' },
+ });
+
+ fireEvent.change(screen.getByLabelText('Custom alias (optional):'), {
+ target: { value: 'my-alias' },
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Shorten URL' }));
+
+ await waitFor(() => {
+ expect(shortenUrl).toHaveBeenCalledWith({
+ fullUrl: 'https://example.com/path',
+ customAlias: 'my-alias',
+ });
+ });
+
+ const link = await screen.findByRole('link', { name: 'http://localhost:8081/my-alias' });
+ expect(link.getAttribute('href')).toBe('http://localhost:8081/my-alias');
+ });
+
+ it('shows ErrorModal when ApiError is thrown and closes it', async () => {
+ vi.mocked(shortenUrl).mockRejectedValue(
+ new ApiError({
+ status: 409,
+ error: 'Conflict',
+ message: 'Alias already exists',
+ timestamp: '2026-08-31T00:00:00.000Z',
+ }),
+ );
+
+ render();
+
+ fireEvent.change(screen.getByLabelText('Enter URL to shorten:'), {
+ target: { value: 'https://example.com/path' },
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Shorten URL' }));
+
+ const alert = await screen.findByRole('alert');
+ expect(alert.textContent).toContain('Alias already exists');
+
+ fireEvent.click(screen.getByRole('button', { name: 'Close' }));
+
+ await waitFor(() => {
+ expect(screen.queryByRole('alert')).toBeNull();
+ });
+ });
+});
diff --git a/frontend/src/components/UrlList.integration.test.tsx b/frontend/src/components/UrlList.integration.test.tsx
new file mode 100644
index 00000000..3aa39769
--- /dev/null
+++ b/frontend/src/components/UrlList.integration.test.tsx
@@ -0,0 +1,178 @@
+import React from 'react';
+import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import UrlList from './UrlList';
+import type { UrlRecord } from '../types/UrlRecord';
+
+type PageResponse = {
+ content: T[];
+ totalPages: number;
+ totalElements: number;
+ number: number;
+ size: number;
+ first: boolean;
+ last: boolean;
+ numberOfElements: number;
+};
+
+function buildPageResponse(content: UrlRecord[], page = 0, totalPages = 1, last = true): PageResponse {
+ return {
+ content,
+ totalPages,
+ totalElements: content.length,
+ number: page,
+ size: 10,
+ first: page === 0,
+ last,
+ numberOfElements: content.length,
+ };
+}
+
+describe('UrlList integration', () => {
+ const originalFetch = global.fetch;
+
+ beforeEach(() => {
+ global.fetch = vi.fn();
+ });
+
+ afterEach(() => {
+ global.fetch = originalFetch;
+ vi.restoreAllMocks();
+ });
+
+ it('re-fetches data when refreshToken prop changes', async () => {
+ vi.mocked(global.fetch).mockResolvedValue(
+ new Response(
+ JSON.stringify(
+ buildPageResponse([
+ {
+ alias: 'one',
+ shortUrl: 'http://localhost:8081/one',
+ actualUrl: 'https://example.com/one',
+ },
+ ]),
+ ),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ ),
+ );
+
+ const { rerender } = render();
+ await screen.findByText('one');
+
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+
+ rerender();
+
+ await waitFor(() => {
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ it('fetches with filter and next-page parameters', async () => {
+ vi.mocked(global.fetch).mockImplementation(async (input) => {
+ const requestUrl = String(input);
+
+ if (requestUrl.includes('page=1')) {
+ return new Response(
+ JSON.stringify(
+ buildPageResponse(
+ [
+ {
+ alias: 'app-2',
+ shortUrl: 'http://localhost:8081/app-2',
+ actualUrl: 'https://example.com/app-2',
+ },
+ ],
+ 1,
+ 2,
+ true,
+ ),
+ ),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ );
+ }
+
+ return new Response(
+ JSON.stringify(
+ buildPageResponse(
+ [
+ {
+ alias: 'app-1',
+ shortUrl: 'http://localhost:8081/app-1',
+ actualUrl: 'https://example.com/app-1',
+ },
+ ],
+ 0,
+ 2,
+ false,
+ ),
+ ),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ );
+ });
+
+ render();
+
+ await screen.findByText('app-1');
+
+ fireEvent.change(screen.getByPlaceholderText('Filter by alias'), {
+ target: { value: 'app' },
+ });
+
+ await waitFor(() => {
+ expect(vi.mocked(global.fetch).mock.calls.some((call) => String(call[0]).includes('alias=app'))).toBe(true);
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Next' }));
+
+ await screen.findByText('app-2');
+
+ expect(vi.mocked(global.fetch).mock.calls.some((call) => String(call[0]).includes('page=1'))).toBe(true);
+ });
+
+ it('opens delete dialog and sends delete request on confirmation', async () => {
+ vi.mocked(global.fetch).mockImplementation(async (input, init) => {
+ const requestUrl = String(input);
+ const method = init?.method ?? 'GET';
+
+ if (method === 'DELETE' && requestUrl.includes('/api/v1/remove-me')) {
+ return new Response(null, { status: 204 });
+ }
+
+ return new Response(
+ JSON.stringify(
+ buildPageResponse([
+ {
+ alias: 'remove-me',
+ shortUrl: 'http://localhost:8081/remove-me',
+ actualUrl: 'https://example.com/remove-me',
+ },
+ ]),
+ ),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ );
+ });
+
+ render();
+
+ await screen.findByText('remove-me');
+
+ fireEvent.click(screen.getByRole('button', { name: 'Delete' }));
+
+ const confirm = await screen.findByRole('button', { name: 'Confirm Delete' });
+ fireEvent.click(confirm);
+
+ await waitFor(() => {
+ expect(
+ vi.mocked(global.fetch).mock.calls.some((call) => {
+ const method = (call[1] as RequestInit | undefined)?.method;
+ return method === 'DELETE' && String(call[0]).includes('/api/v1/remove-me');
+ }),
+ ).toBe(true);
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog')).toBeNull();
+ });
+ });
+});
diff --git a/frontend/src/components/UrlList.tsx b/frontend/src/components/UrlList.tsx
new file mode 100644
index 00000000..60be4187
--- /dev/null
+++ b/frontend/src/components/UrlList.tsx
@@ -0,0 +1,260 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { ApiError, deleteUrl, getUrlsPage, type PageResponse } from '../api/apiCaller';
+import type { UrlRecord } from '../types/UrlRecord';
+
+type UrlListProps = {
+ refreshToken?: number;
+};
+
+function getAlias(item: UrlRecord): string {
+ if (item.alias) return item.alias;
+
+ try {
+ const url = new URL(item.shortUrl);
+ return url.pathname.split('/').filter(Boolean).pop() || item.shortUrl;
+ } catch {
+ return item.shortUrl;
+ }
+}
+
+function UrlList({ refreshToken = 0 }: UrlListProps): React.ReactElement {
+ const [filter, setFilter] = useState('');
+ const [page, setPage] = useState(0);
+ const [pageSize, setPageSize] = useState(10);
+ const [reloadToken, setReloadToken] = useState(0);
+ const [pageData, setPageData] = useState>({
+ content: [],
+ totalPages: 0,
+ totalElements: 0,
+ number: 0,
+ size: 10,
+ first: true,
+ last: true,
+ numberOfElements: 0,
+ });
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [confirmDelete, setConfirmDelete] = useState(null);
+
+ useEffect(() => {
+ const fetchUrls = async () => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const data = await getUrlsPage({
+ page,
+ size: pageSize,
+ alias: filter.trim() || undefined,
+ });
+ setPageData(data);
+ } catch (err) {
+ if (err instanceof ApiError) {
+ setError(err.details.message);
+ } else {
+ setError(err instanceof Error ? err.message : 'Something went wrong');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchUrls();
+ }, [page, pageSize, filter, refreshToken, reloadToken]);
+
+ const filteredUrls = useMemo(() => {
+ return pageData.content.filter((item) => {
+ const alias = getAlias(item).toLowerCase();
+ return alias.includes(filter.trim().toLowerCase());
+ });
+ }, [pageData.content, filter]);
+
+ const handleDelete = async () => {
+ if (!confirmDelete) return;
+
+ const alias = getAlias(confirmDelete);
+
+ try {
+ await deleteUrl(alias);
+
+ setConfirmDelete(null);
+
+ if (pageData.content.length === 1 && page > 0) {
+ setPage(page - 1);
+ } else {
+ setReloadToken((prev) => prev + 1);
+ }
+ } catch (err) {
+ if (err instanceof ApiError) {
+ setError(err.details.message);
+ } else {
+ setError(err instanceof Error ? err.message : 'Unable to delete');
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
History
+
Shortened URLs
+
+
{pageData.totalElements} total records
+
+
+
+ {
+ setFilter(e.target.value);
+ setPage(0);
+ }}
+ placeholder="Filter by alias"
+ className="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-cyan-500 focus:ring-4 focus:ring-cyan-100"
+ />
+
+
+
+
+ {error && (
+
{error}
+ )}
+
+ {loading ? (
+
Loading...
+ ) : (
+ <>
+
+
+
+
+ | Alias |
+ Original URL |
+ Short URL |
+ Action |
+
+
+
+ {filteredUrls.length === 0 ? (
+
+ | No records found |
+
+ ) : (
+ filteredUrls.map((item) => {
+ const alias = getAlias(item);
+
+ return (
+
+ | {alias} |
+
+
+ {item.actualUrl}
+
+ |
+
+
+ {item.shortUrl}
+
+ |
+
+
+ |
+
+ );
+ })
+ )}
+
+
+
+
+
+
+ Page {pageData.number + 1} of {pageData.totalPages || 1}
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {confirmDelete && (
+
+
+ Delete alias {getAlias(confirmDelete)}?
+
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default UrlList;
\ No newline at end of file
diff --git a/frontend/src/components/UrlList.unit.test.tsx b/frontend/src/components/UrlList.unit.test.tsx
new file mode 100644
index 00000000..f504a6a6
--- /dev/null
+++ b/frontend/src/components/UrlList.unit.test.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import UrlList from './UrlList';
+import type { UrlRecord } from '../types/UrlRecord';
+
+type PageResponse = {
+ content: T[];
+ totalPages: number;
+ totalElements: number;
+ number: number;
+ size: number;
+ first: boolean;
+ last: boolean;
+ numberOfElements: number;
+};
+
+function buildPageResponse(content: UrlRecord[]): PageResponse {
+ return {
+ content,
+ totalPages: 1,
+ totalElements: content.length,
+ number: 0,
+ size: 10,
+ first: true,
+ last: true,
+ numberOfElements: content.length,
+ };
+}
+
+describe('UrlList unit', () => {
+ const originalFetch = global.fetch;
+
+ beforeEach(() => {
+ global.fetch = vi.fn();
+ });
+
+ afterEach(() => {
+ global.fetch = originalFetch;
+ vi.restoreAllMocks();
+ });
+
+ it('renders a derived alias when alias field is missing', async () => {
+ vi.mocked(global.fetch).mockResolvedValue(
+ new Response(
+ JSON.stringify(
+ buildPageResponse([
+ {
+ shortUrl: 'http://localhost:8081/my-alias',
+ actualUrl: 'https://example.com/articles/1',
+ },
+ ]),
+ ),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ ),
+ );
+
+ render();
+
+ const aliasCell = await screen.findByText('my-alias');
+ expect(aliasCell.textContent).toBe('my-alias');
+ expect(screen.getByText('https://example.com/articles/1')).toBeTruthy();
+ expect(screen.getByText('http://localhost:8081/my-alias')).toBeTruthy();
+ });
+
+ it('shows empty-state message when no records are returned', async () => {
+ vi.mocked(global.fetch).mockResolvedValue(
+ new Response(JSON.stringify(buildPageResponse([])), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ }),
+ );
+
+ render();
+
+ const empty = await screen.findByText('No records found');
+ expect(empty.textContent).toContain('No records found');
+ });
+
+ it('shows an error message when fetch fails', async () => {
+ vi.mocked(global.fetch).mockResolvedValue(
+ new Response('server failed', {
+ status: 500,
+ statusText: 'Internal Server Error',
+ }),
+ );
+
+ render();
+
+ const errorText = await screen.findByText('server failed');
+ expect(errorText.textContent).toContain('server failed');
+ });
+});
diff --git a/frontend/src/components/error/ErrorModal.tsx b/frontend/src/components/error/ErrorModal.tsx
new file mode 100644
index 00000000..489c58bb
--- /dev/null
+++ b/frontend/src/components/error/ErrorModal.tsx
@@ -0,0 +1,33 @@
+
+import React from "react";
+
+type ErrorModalProps = {
+ title: string;
+ message: string;
+ status: number;
+ error: string;
+ timestamp: string;
+ onConfirm: () => void;
+};
+
+function ErrorModal({ title, message, status, error, timestamp, onConfirm}: ErrorModalProps): React.ReactElement {
+ console.log('ErrorModal props:', { title, message, status, error, timestamp });
+ return (
+
+
{title} - {error}
+
{message}
+
Status: {status} | {new Date(timestamp).toLocaleString()}
+
+
+
+
+ );
+}
+
+export default ErrorModal;
\ No newline at end of file
diff --git a/frontend/src/components/todo.txt b/frontend/src/components/todo.txt
new file mode 100644
index 00000000..902f1548
--- /dev/null
+++ b/frontend/src/components/todo.txt
@@ -0,0 +1,10 @@
+
+====================================================================
+-> Apply Error handling :: done
+-> Write Unit testing for the URLShortner :: done
+-> Add the second part of the code
+ -> The URL display :: done
+ -> Delete :: done
+ -> Unit testing
+-> Add the CSS file for the styling
+====================================================================
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 00000000..252e3691
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,13 @@
+@import "tailwindcss";
+
+@layer base {
+ body {
+ margin: 0;
+ font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
+ background:
+ radial-gradient(circle at 18% 8%, #e7f3ff 0%, transparent 38%),
+ radial-gradient(circle at 80% 94%, #f4f8e7 0%, transparent 34%),
+ #f8fafc;
+ color: #0f172a;
+ }
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 00000000..bef5202a
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts
new file mode 100644
index 00000000..66eef4d6
--- /dev/null
+++ b/frontend/src/test/setup.ts
@@ -0,0 +1,6 @@
+import { afterEach } from 'vitest';
+import { cleanup } from '@testing-library/react';
+
+afterEach(() => {
+ cleanup();
+});
diff --git a/frontend/src/types/ApiErrorResponse.ts b/frontend/src/types/ApiErrorResponse.ts
new file mode 100644
index 00000000..9c90089f
--- /dev/null
+++ b/frontend/src/types/ApiErrorResponse.ts
@@ -0,0 +1,8 @@
+type ApiErrorResponse = {
+ status: number;
+ error: string;
+ message: string;
+ timestamp: string;
+};
+
+export type { ApiErrorResponse };
\ No newline at end of file
diff --git a/frontend/src/types/UrlRecord.ts b/frontend/src/types/UrlRecord.ts
new file mode 100644
index 00000000..497614a4
--- /dev/null
+++ b/frontend/src/types/UrlRecord.ts
@@ -0,0 +1,5 @@
+export type UrlRecord = {
+ shortUrl: string;
+ actualUrl: string;
+ alias?: string;
+};
diff --git a/frontend/src/types/url.ts b/frontend/src/types/url.ts
new file mode 100644
index 00000000..2c793eac
--- /dev/null
+++ b/frontend/src/types/url.ts
@@ -0,0 +1,18 @@
+export type UrlRequest = {
+ fullUrl: string;
+ customAlias?: string;
+};
+
+export type UrlResponse = {
+ customAlias: string;
+ fullUrl: string;
+ shortUrl: string;
+ createdAt?: string;
+};
+
+export type UrlItem = {
+ customAlias: string;
+ fullUrl: string;
+ shortUrl?: string;
+ createdAt?: string;
+};
diff --git a/frontend/src/types/urlApi.ts b/frontend/src/types/urlApi.ts
new file mode 100644
index 00000000..f65666c2
--- /dev/null
+++ b/frontend/src/types/urlApi.ts
@@ -0,0 +1,18 @@
+export type ShortenUrlRequest = {
+ fullUrl: string;
+ customAlias?: string;
+};
+
+export type ShortenUrlResponse = {
+ customAlias: string;
+ fullUrl: string;
+ shortUrl: string;
+ createdAt?: string;
+};
+
+export type UrlListItem = {
+ customAlias: string;
+ fullUrl: string;
+ shortUrl?: string;
+ createdAt?: string;
+};
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 00000000..cec28ce8
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "exclude": [
+ "src/**/*.test.ts",
+ "src/**/*.test.tsx",
+ "src/**/*.spec.ts",
+ "src/**/*.spec.tsx"
+ ]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 00000000..8455dcbc
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 00000000..a3a66285
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,8 @@
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+import { defineConfig } from 'vite'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+})
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
new file mode 100644
index 00000000..fca0ac0b
--- /dev/null
+++ b/frontend/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ environment: 'jsdom',
+ setupFiles: './src/test/setup.ts',
+ clearMocks: true,
+ },
+});
diff --git a/my-apporach.md b/my-apporach.md
new file mode 100644
index 00000000..ac7bccbd
--- /dev/null
+++ b/my-apporach.md
@@ -0,0 +1,70 @@
+# URL Shortener System Design
+
+## 1. Core Workflows
+
+### 1.1 Short URL Generation (No Alias Provided)
+
+When a user requests a shortened URL without specifying a custom alias, the system handles the creation automatically:
+
+- **Client Request:** The user submits a request containing the destination `long_url`.
+- **Code Generation:** The system automatically computes a unique, 7-character alphanumeric string.
+- **URL Construction:** This 7-character string is appended to the base domain to create the final short URL.
+- **Persistence:** The mapping of the generated key, `long_url`, and metadata is saved to the SQLite database.
+
+### 1.2 Short URL Generation (Custom Alias Provided)
+
+When a user wants a personalized or branded link, the application performs an explicit verification:
+
+- **Availability Check:** The application queries the SQLite database to see if the requested alias string already exists.
+- **Handling Duplicates:** If the alias is already allocated, the system rejects the request and throws a conflict error.
+- **Successful Assignment:** If available, the application reserves the alias, links it to the target `long_url`, and commits the record to the database.
+
+---
+
+## 2. Alphanumeric Code Generation Strategy
+
+The generation of the unique 7-character string relies on a two-step translation process:
+
+### Step 1: Unique Numeric ID Generation
+
+To ensure sequential distribution and eliminate collisions, the application uses a lightweight variant of the **Twitter Snowflake ID** algorithm.
+
+- It generates a **64-bit unsigned integer** that guarantees global uniqueness without requiring expensive database lookups.
+
+### Step 2: Base62 Alphanumeric Encoding
+
+The unique 64-bit integer is converted into a compact, URL-safe string format:
+
+- **Character Set:** A pre-defined character array containing 62 characters: numbers (`0-9`), lowercase letters (`a-z`), and uppercase letters (`A-Z`).
+- **Conversion Math:** The system loops exactly 7 times to enforce a strict **7-character fixed size** string. In each iteration, a modulo 62 operation (`id % 62`) determines the remainder.
+- **Array Indexing:** This remainder acts as the exact array index to extract the character, after which the numeric ID is divided by 62 (`id / 62`) for the subsequent iteration step.
+
+---
+
+## 3. Cross-Cutting Concerns
+
+### 3.1 Global Exception Handling
+
+- An application-wide exception middleware interceptor captures runtime errors.
+- It formats errors gracefully into standardized JSON payloads (e.g., handling validation errors for taken aliases or unexpected database connection losses).
+
+### 3.2 In-Memory Caching (Dual LRU Strategy)
+
+To optimize data retrieval times and minimize raw database reads, the system uses two separate **Least Recently Used (LRU)** caches:
+
+- **Short-to-Long Cache:** Bypasses database lookups during incoming user redirections by mapping `short_url` -> `long_url`.
+- **Long-to-Short Cache:** Avoids duplicating work during write operations by immediately resolving `long_url` -> `short_url` if the link was recently processed.
+
+---
+
+## 4. Database Schema
+
+### Table: `url_details`
+
+| Column Name | Data Type | Constraints | Notes |
+| :------------- | :-------- | :------------------------ | :--------------------------------------------- |
+| `id` | INTEGER | PRIMARY KEY | Unique system ID |
+| `actual_url` | TEXT | UNIQUE, NOT NULL | The destination web address |
+| `shortend_url` | TEXT | UNIQUE, NOT NULL | The generated 7-character code or custom alias |
+| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | Automatically set on creation |
+| `updated_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | Automatically modified on update |