✨ feat: Add more search criteria.
This commit is contained in:
parent
493392ed07
commit
4a907a611d
16 changed files with 1820 additions and 310 deletions
|
|
@ -75,4 +75,14 @@ public class MessyUtils {
|
||||||
});
|
});
|
||||||
return completableFuture.join();
|
return completableFuture.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long convertToByte(long value, String unit) {
|
||||||
|
return switch (unit) {
|
||||||
|
case "B" -> value;
|
||||||
|
case "KB" -> value * 1024;
|
||||||
|
case "MB" -> value * 1024 * 1024;
|
||||||
|
case "GB" -> value * 1024 * 1024 * 1024;
|
||||||
|
default -> throw new IllegalArgumentException("Unknown unit: " + unit);
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,8 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Future<JsonObject> getChatFiles(long chatId, Map<String, String> filter) {
|
public Future<JsonObject> getChatFiles(long chatId, Map<String, String> filter) {
|
||||||
String downloadStatus = filter.get("downloadStatus");
|
boolean offline = Convert.toBool(filter.get("offline"), false);
|
||||||
if (Arrays.asList("downloading", "paused", "completed", "error").contains(downloadStatus)) {
|
if (offline) {
|
||||||
return DataVerticle.fileRepository.getFiles(chatId, filter)
|
return DataVerticle.fileRepository.getFiles(chatId, filter)
|
||||||
.compose(r -> {
|
.compose(r -> {
|
||||||
long[] messageIds = r.v1.stream().mapToLong(FileRecord::messageId).toArray();
|
long[] messageIds = r.v1.stream().mapToLong(FileRecord::messageId).toArray();
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ import telegram.files.MessyUtils;
|
||||||
import telegram.files.repository.FileRecord;
|
import telegram.files.repository.FileRecord;
|
||||||
import telegram.files.repository.FileRepository;
|
import telegram.files.repository.FileRepository;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
@ -83,16 +87,35 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<Tuple3<List<FileRecord>, Long, Long>> getFiles(long chatId, Map<String, String> filter) {
|
public Future<Tuple3<List<FileRecord>, Long, Long>> getFiles(long chatId, Map<String, String> filter) {
|
||||||
|
String search = filter.get("search");
|
||||||
|
String type = filter.get("type");
|
||||||
String downloadStatus = filter.get("downloadStatus");
|
String downloadStatus = filter.get("downloadStatus");
|
||||||
String transferStatus = filter.get("transferStatus");
|
String transferStatus = filter.get("transferStatus");
|
||||||
String search = filter.get("search");
|
String dateType = filter.get("dateType");
|
||||||
|
String dateRange = filter.get("dateRange");
|
||||||
|
String sizeRange = filter.get("sizeRange");
|
||||||
|
String sizeUnit = filter.get("sizeUnit");
|
||||||
|
String sort = filter.get("sort");
|
||||||
|
String order = filter.get("order");
|
||||||
|
|
||||||
Long fromMessageId = Convert.toLong(filter.get("fromMessageId"), 0L);
|
Long fromMessageId = Convert.toLong(filter.get("fromMessageId"), 0L);
|
||||||
String type = filter.get("type");
|
|
||||||
int limit = Convert.toInt(filter.get("limit"), 20);
|
int limit = Convert.toInt(filter.get("limit"), 20);
|
||||||
|
|
||||||
String whereClause = "chat_id = #{chatId}";
|
String whereClause = "chat_id = #{chatId}";
|
||||||
Map<String, Object> params = MapUtil.of("chatId", chatId);
|
Map<String, Object> params = MapUtil.of("chatId", chatId);
|
||||||
params.put("limit", limit);
|
params.put("limit", limit);
|
||||||
|
if (StrUtil.isNotBlank(search)) {
|
||||||
|
whereClause += " AND (file_name LIKE #{search} OR caption LIKE #{search})";
|
||||||
|
params.put("search", "%%" + search + "%%");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(type) && !Objects.equals(type, "all")) {
|
||||||
|
if (Objects.equals(type, "media")) {
|
||||||
|
whereClause += " AND type IN ('photo', 'video')";
|
||||||
|
} else {
|
||||||
|
whereClause += " AND type = #{type}";
|
||||||
|
params.put("type", type);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (StrUtil.isNotBlank(downloadStatus)) {
|
if (StrUtil.isNotBlank(downloadStatus)) {
|
||||||
whereClause += " AND download_status = #{downloadStatus}";
|
whereClause += " AND download_status = #{downloadStatus}";
|
||||||
params.put("downloadStatus", downloadStatus);
|
params.put("downloadStatus", downloadStatus);
|
||||||
|
|
@ -101,16 +124,34 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
whereClause += " AND transfer_status = #{transferStatus}";
|
whereClause += " AND transfer_status = #{transferStatus}";
|
||||||
params.put("transferStatus", transferStatus);
|
params.put("transferStatus", transferStatus);
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(search)) {
|
if (StrUtil.isNotBlank(dateType) && StrUtil.isNotBlank(dateRange)) {
|
||||||
whereClause += " AND (file_name LIKE #{search} OR caption LIKE #{search})";
|
String[] dates = dateRange.split(",");
|
||||||
params.put("search", "%%" + search + "%%");
|
if (dates.length == 2) {
|
||||||
|
long startTime = LocalDate.parse(dates[0], DateTimeFormatter.ISO_DATE)
|
||||||
|
.atStartOfDay()
|
||||||
|
.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||||
|
long endTime = LocalDate.parse(dates[1], DateTimeFormatter.ISO_DATE)
|
||||||
|
.atTime(LocalTime.MAX)
|
||||||
|
.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||||
|
if (Objects.equals(dateType, "sent")) {
|
||||||
|
whereClause += " AND date >= #{startTime} AND date <= #{endTime}";
|
||||||
|
startTime = startTime / 1000;
|
||||||
|
endTime = endTime / 1000;
|
||||||
|
} else {
|
||||||
|
whereClause += " AND completion_date >= #{startTime} AND completion_date <= #{endTime}";
|
||||||
|
}
|
||||||
|
params.put("startTime", startTime);
|
||||||
|
params.put("endTime", endTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(type)) {
|
if (StrUtil.isNotBlank(sizeRange) && StrUtil.isNotBlank(sizeUnit)) {
|
||||||
if (Objects.equals(type, "media")) {
|
String[] sizes = sizeRange.split(",");
|
||||||
whereClause += " AND type IN ('photo', 'video')";
|
if (sizes.length == 2) {
|
||||||
} else {
|
long minSize = MessyUtils.convertToByte(Convert.toLong(sizes[0]), sizeUnit);
|
||||||
whereClause += " AND type = #{type}";
|
long maxSize = MessyUtils.convertToByte(Convert.toLong(sizes[1]), sizeUnit);
|
||||||
params.put("type", type);
|
whereClause += " AND size >= #{minSize} AND size <= #{maxSize}";
|
||||||
|
params.put("minSize", minSize);
|
||||||
|
params.put("maxSize", maxSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String countClause = whereClause;
|
String countClause = whereClause;
|
||||||
|
|
@ -118,11 +159,16 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
whereClause += " AND message_id < #{fromMessageId}";
|
whereClause += " AND message_id < #{fromMessageId}";
|
||||||
params.put("fromMessageId", fromMessageId);
|
params.put("fromMessageId", fromMessageId);
|
||||||
}
|
}
|
||||||
|
String orderBy = "message_id DESC";
|
||||||
|
if (StrUtil.isNotBlank(sort) && StrUtil.isNotBlank(order)) {
|
||||||
|
orderBy = "%s %s".formatted(sort, order);
|
||||||
|
}
|
||||||
|
log.trace("Get files with where: %s params: %s".formatted(whereClause, params));
|
||||||
return Future.all(
|
return Future.all(
|
||||||
SqlTemplate
|
SqlTemplate
|
||||||
.forQuery(pool, """
|
.forQuery(pool, """
|
||||||
SELECT * FROM file_record WHERE %s ORDER BY message_id desc LIMIT #{limit}
|
SELECT * FROM file_record WHERE %s ORDER BY %s LIMIT #{limit}
|
||||||
""".formatted(whereClause))
|
""".formatted(whereClause, orderBy))
|
||||||
.mapTo(FileRecord.ROW_MAPPER)
|
.mapTo(FileRecord.ROW_MAPPER)
|
||||||
.execute(params)
|
.execute(params)
|
||||||
.onFailure(err -> log.error("Failed to get file record: %s".formatted(err.getMessage())))
|
.onFailure(err -> log.error("Failed to get file record: %s".formatted(err.getMessage())))
|
||||||
|
|
|
||||||
719
web/package-lock.json
generated
719
web/package-lock.json
generated
|
|
@ -1,15 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "telegram-files-web",
|
"name": "telegram-files-web",
|
||||||
"version": "0.1.13",
|
"version": "0.1.14",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "telegram-files-web",
|
"name": "telegram-files-web",
|
||||||
"version": "0.1.13",
|
"version": "0.1.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.2.0",
|
"@dnd-kit/core": "^6.2.0",
|
||||||
"@dnd-kit/modifiers": "^8.0.0",
|
|
||||||
"@dnd-kit/sortable": "^9.0.0",
|
"@dnd-kit/sortable": "^9.0.0",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
|
|
@ -20,11 +19,13 @@
|
||||||
"@radix-ui/react-hover-card": "^1.1.4",
|
"@radix-ui/react-hover-card": "^1.1.4",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-popover": "^1.1.2",
|
"@radix-ui/react-popover": "^1.1.2",
|
||||||
|
"@radix-ui/react-portal": "^1.1.4",
|
||||||
"@radix-ui/react-progress": "^1.1.0",
|
"@radix-ui/react-progress": "^1.1.0",
|
||||||
|
"@radix-ui/react-radio-group": "^1.2.3",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
"@radix-ui/react-slider": "^1.2.3",
|
"@radix-ui/react-slider": "^1.2.3",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
|
|
@ -35,7 +36,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"geist": "^1.3.0",
|
"geist": "^1.3.0",
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
|
|
@ -48,6 +49,7 @@
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"qr-code-styling": "^1.9.1",
|
"qr-code-styling": "^1.9.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.2",
|
"react-hook-form": "^7.53.2",
|
||||||
"react-use-websocket": "^4.11.1",
|
"react-use-websocket": "^4.11.1",
|
||||||
|
|
@ -124,19 +126,6 @@
|
||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@dnd-kit/modifiers": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-oPZ0JoKtVSK9hVHSBDKG1oCLgnZbpxuH/SMnyDtXkNhn9+SF1+98DlWILFYxIT8faOS/GgfhlNFREmym6oqrEg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@dnd-kit/core": "^6.2.0",
|
|
||||||
"react": ">=16.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@dnd-kit/sortable": {
|
"node_modules/@dnd-kit/sortable": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-9.0.0.tgz",
|
||||||
|
|
@ -1318,6 +1307,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
||||||
|
|
@ -1966,6 +1973,48 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popover": {
|
"node_modules/@radix-ui/react-popover": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
|
||||||
|
|
@ -2002,6 +2051,48 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||||
|
|
@ -2048,11 +2139,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-portal": {
|
"node_modules/@radix-ui/react-portal": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
||||||
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-primitive": "2.0.0",
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
@ -2070,6 +2162,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-presence": {
|
"node_modules/@radix-ui/react-presence": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
|
||||||
|
|
@ -2115,6 +2230,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-progress": {
|
"node_modules/@radix-ui/react-progress": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz",
|
||||||
|
|
@ -2152,6 +2285,163 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.2",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-collection": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-roving-focus": {
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
|
||||||
|
|
@ -2238,6 +2528,48 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-separator": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz",
|
||||||
|
|
@ -2416,7 +2748,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||||
|
|
@ -2434,13 +2766,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
"dependencies": {
|
"license": "MIT",
|
||||||
"@radix-ui/react-compose-refs": "1.1.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
|
@ -2604,6 +2934,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-toggle": {
|
"node_modules/@radix-ui/react-toggle": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.1.tgz",
|
||||||
|
|
@ -2719,6 +3073,48 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||||
|
|
@ -4374,9 +4770,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
"version": "4.1.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
|
@ -7132,6 +7529,20 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-day-picker": {
|
||||||
|
"version": "8.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
|
||||||
|
"integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/gpbl"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"date-fns": "^2.28.0 || ^3.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
|
@ -8635,15 +9046,6 @@
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@dnd-kit/modifiers": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-oPZ0JoKtVSK9hVHSBDKG1oCLgnZbpxuH/SMnyDtXkNhn9+SF1+98DlWILFYxIT8faOS/GgfhlNFREmym6oqrEg==",
|
|
||||||
"requires": {
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@dnd-kit/sortable": {
|
"@dnd-kit/sortable": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-9.0.0.tgz",
|
||||||
|
|
@ -9293,6 +9695,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
||||||
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -9597,6 +10007,25 @@
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
"aria-hidden": "^1.1.1",
|
"aria-hidden": "^1.1.1",
|
||||||
"react-remove-scroll": "2.6.0"
|
"react-remove-scroll": "2.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-popover": {
|
"@radix-ui/react-popover": {
|
||||||
|
|
@ -9619,6 +10048,25 @@
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
"aria-hidden": "^1.1.1",
|
"aria-hidden": "^1.1.1",
|
||||||
"react-remove-scroll": "2.6.0"
|
"react-remove-scroll": "2.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-popper": {
|
"@radix-ui/react-popper": {
|
||||||
|
|
@ -9647,12 +10095,22 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-portal": {
|
"@radix-ui/react-portal": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
||||||
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@radix-ui/react-primitive": "2.0.0",
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-presence": {
|
"@radix-ui/react-presence": {
|
||||||
|
|
@ -9670,6 +10128,16 @@
|
||||||
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@radix-ui/react-slot": "1.1.0"
|
"@radix-ui/react-slot": "1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-progress": {
|
"@radix-ui/react-progress": {
|
||||||
|
|
@ -9689,6 +10157,80 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@radix-ui/react-radio-group": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.2",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
|
||||||
|
},
|
||||||
|
"@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-collection": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@radix-ui/react-roving-focus": {
|
"@radix-ui/react-roving-focus": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
|
||||||
|
|
@ -9739,6 +10281,25 @@
|
||||||
"@radix-ui/react-visually-hidden": "1.1.0",
|
"@radix-ui/react-visually-hidden": "1.1.0",
|
||||||
"aria-hidden": "^1.1.1",
|
"aria-hidden": "^1.1.1",
|
||||||
"react-remove-scroll": "2.6.0"
|
"react-remove-scroll": "2.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-separator": {
|
"@radix-ui/react-separator": {
|
||||||
|
|
@ -9820,23 +10381,23 @@
|
||||||
"requires": {
|
"requires": {
|
||||||
"@radix-ui/react-slot": "1.1.2"
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"@radix-ui/react-slot": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
|
||||||
"requires": {
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-slot": {
|
"@radix-ui/react-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.0"
|
"@radix-ui/react-compose-refs": "1.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
|
"requires": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-switch": {
|
"@radix-ui/react-switch": {
|
||||||
|
|
@ -9914,6 +10475,17 @@
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||||
"@radix-ui/react-visually-hidden": "1.1.0"
|
"@radix-ui/react-visually-hidden": "1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-toggle": {
|
"@radix-ui/react-toggle": {
|
||||||
|
|
@ -9972,6 +10544,25 @@
|
||||||
"@radix-ui/react-slot": "1.1.0",
|
"@radix-ui/react-slot": "1.1.0",
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
"@radix-ui/react-visually-hidden": "1.1.0"
|
"@radix-ui/react-visually-hidden": "1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"requires": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@radix-ui/react-use-callback-ref": {
|
"@radix-ui/react-use-callback-ref": {
|
||||||
|
|
@ -11024,9 +11615,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"date-fns": {
|
"date-fns": {
|
||||||
"version": "4.1.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
|
|
@ -12890,6 +13481,12 @@
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-day-picker": {
|
||||||
|
"version": "8.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
|
||||||
|
"integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.2.0",
|
"@dnd-kit/core": "^6.2.0",
|
||||||
"@dnd-kit/modifiers": "^8.0.0",
|
|
||||||
"@dnd-kit/sortable": "^9.0.0",
|
"@dnd-kit/sortable": "^9.0.0",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
|
|
@ -28,11 +27,13 @@
|
||||||
"@radix-ui/react-hover-card": "^1.1.4",
|
"@radix-ui/react-hover-card": "^1.1.4",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-popover": "^1.1.2",
|
"@radix-ui/react-popover": "^1.1.2",
|
||||||
|
"@radix-ui/react-portal": "^1.1.4",
|
||||||
"@radix-ui/react-progress": "^1.1.0",
|
"@radix-ui/react-progress": "^1.1.0",
|
||||||
|
"@radix-ui/react-radio-group": "^1.2.3",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
"@radix-ui/react-slider": "^1.2.3",
|
"@radix-ui/react-slider": "^1.2.3",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
|
|
@ -43,7 +44,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"geist": "^1.3.0",
|
"geist": "^1.3.0",
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
|
|
@ -56,6 +57,7 @@
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"qr-code-styling": "^1.9.1",
|
"qr-code-styling": "^1.9.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.2",
|
"react-hook-form": "^7.53.2",
|
||||||
"react-use-websocket": "^4.11.1",
|
"react-use-websocket": "^4.11.1",
|
||||||
|
|
|
||||||
|
|
@ -1,215 +1,559 @@
|
||||||
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
type DownloadStatus,
|
type ChangeEvent,
|
||||||
type FileFilter,
|
type CSSProperties,
|
||||||
type TransferStatus,
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
ArrowDownNarrowWide,
|
||||||
|
ArrowUpNarrowWide,
|
||||||
|
Calendar as CalendarRange,
|
||||||
|
Filter,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type {
|
||||||
|
DownloadStatus,
|
||||||
|
FileFilter,
|
||||||
|
FileType,
|
||||||
|
TransferStatus,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Button } from "./ui/button";
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerDescription,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
} from "./ui/drawer";
|
||||||
|
import { Drawer as DrawerPrimitive } from "vaul";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectGroup,
|
SelectItem,
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import TableColumnFilter, {
|
import { Label } from "@/components/ui/label";
|
||||||
type Column,
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
} from "@/components/table-column-filter";
|
import {
|
||||||
import { type RowHeight } from "@/components/table-row-height-switch";
|
Popover,
|
||||||
import FileTypeFilter from "@/components/file-type-filter";
|
PopoverContent,
|
||||||
import dynamic from "next/dynamic";
|
PopoverTrigger,
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
} from "@/components/ui/popover";
|
||||||
import * as React from "react";
|
import { RangeSlider } from "@/components/ui/slider";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { Check, X } from "lucide-react";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { DOWNLOAD_STATUS, TRANSFER_STATUS } from "@/components/file-status";
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import FileTypeFilter from "@/components/file-type-filter";
|
||||||
|
import FileStatusFilter from "@/components/file-status-filter";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import useIsMobile from "@/hooks/use-is-mobile";
|
import useIsMobile from "@/hooks/use-is-mobile";
|
||||||
|
|
||||||
|
const SearchFilter = ({
|
||||||
|
search,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
search: string;
|
||||||
|
onChange: (search: string) => void;
|
||||||
|
}) => {
|
||||||
|
const [localSearch, setLocalSearch] = useState(search);
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setLocalSearch(e.target.value);
|
||||||
|
onChange(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Keyword</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
placeholder="Search with name or caption"
|
||||||
|
value={localSearch}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{search && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-2 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full text-gray-500 transition-all duration-200 hover:scale-110 hover:bg-gray-100 hover:text-gray-800"
|
||||||
|
onClick={() => setLocalSearch("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DateFilterProps {
|
||||||
|
dateType: "sent" | "downloaded" | undefined;
|
||||||
|
dateRange: [string, string] | undefined;
|
||||||
|
onChange: (type: "sent" | "downloaded", range: [string, string]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateFilter = ({ dateType, dateRange, onChange }: DateFilterProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [localType, setLocalType] = useState<"sent" | "downloaded">(
|
||||||
|
dateType ?? "sent",
|
||||||
|
);
|
||||||
|
const [localRange, setLocalRange] = useState<
|
||||||
|
[Date | undefined, Date | undefined]
|
||||||
|
>([
|
||||||
|
dateRange?.[0] ? new Date(dateRange[0]) : undefined,
|
||||||
|
dateRange?.[1] ? new Date(dateRange[1]) : undefined,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleTypeChange = (type: "sent" | "downloaded") => {
|
||||||
|
setLocalType(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRangeSelect = (range?: {
|
||||||
|
from: Date | undefined;
|
||||||
|
to?: Date | undefined;
|
||||||
|
}) => {
|
||||||
|
if (!range) return;
|
||||||
|
|
||||||
|
setLocalRange([range.from, range.to]);
|
||||||
|
if (range.from && range.to) {
|
||||||
|
onChange(localType, [
|
||||||
|
format(range.from, "yyyy-MM-dd"),
|
||||||
|
format(range.to, "yyyy-MM-dd"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayText = () => {
|
||||||
|
if (!dateRange?.[0] && !dateRange?.[1]) return "Select date range";
|
||||||
|
if (dateRange[0] && dateRange[1]) {
|
||||||
|
return `${format(new Date(dateRange[0]), "LLL dd, y")} - ${format(new Date(dateRange[1]), "LLL dd, y")}`;
|
||||||
|
}
|
||||||
|
return "Date range selected";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Date Filter</Label>
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left font-normal"
|
||||||
|
>
|
||||||
|
<CalendarRange className="mr-2 h-4 w-4" />
|
||||||
|
<span className="flex-1">{getDisplayText()}</span>
|
||||||
|
<span className="ml-2 rounded bg-zinc-100 px-2 py-0.5 text-xs text-zinc-600">
|
||||||
|
{localType === "downloaded" ? "Download" : "Sent"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-auto p-4"
|
||||||
|
side={isMobile ? undefined : "right"}
|
||||||
|
modal={true}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={localType === "sent" ? "default" : "outline"}
|
||||||
|
onClick={() => handleTypeChange("sent")}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Sent Date
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={localType === "downloaded" ? "default" : "outline"}
|
||||||
|
onClick={() => handleTypeChange("downloaded")}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Downloaded
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border p-2">
|
||||||
|
<Calendar
|
||||||
|
mode="range"
|
||||||
|
selected={{
|
||||||
|
from: localRange[0],
|
||||||
|
to: localRange[1],
|
||||||
|
}}
|
||||||
|
onSelect={handleRangeSelect}
|
||||||
|
numberOfMonths={2}
|
||||||
|
defaultMonth={localRange[0] ?? new Date()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SizeFilterProps {
|
||||||
|
sizeRange: [number, number] | undefined;
|
||||||
|
sizeUnit: "KB" | "MB" | "GB" | undefined;
|
||||||
|
onChange: (range: [number, number], unit: "KB" | "MB" | "GB") => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeFilter = ({ sizeRange, onChange }: SizeFilterProps) => {
|
||||||
|
const defaultRange: [number, number] = [0, 1000];
|
||||||
|
const [localRange, setLocalRange] = useState<[number, number]>(
|
||||||
|
sizeRange ?? defaultRange,
|
||||||
|
);
|
||||||
|
const [localUnit, setLocalUnit] = useState<"KB" | "MB" | "GB">("MB");
|
||||||
|
|
||||||
|
const handleChange = (newValue: number[]) => {
|
||||||
|
const range: [number, number] = [newValue[0]!, newValue[1]!];
|
||||||
|
setLocalRange(range);
|
||||||
|
onChange(range, localUnit);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>File Size Range</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
{localUnit}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-2" align="center" modal={true}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={localUnit === "KB" ? "default" : "outline"}
|
||||||
|
onClick={() => setLocalUnit("KB")}
|
||||||
|
>
|
||||||
|
KB
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={localUnit === "MB" ? "default" : "outline"}
|
||||||
|
onClick={() => setLocalUnit("MB")}
|
||||||
|
>
|
||||||
|
MB
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={localUnit === "GB" ? "default" : "outline"}
|
||||||
|
onClick={() => setLocalUnit("GB")}
|
||||||
|
>
|
||||||
|
GB
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="px-2 pt-2"
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RangeSlider
|
||||||
|
value={localRange}
|
||||||
|
min={0}
|
||||||
|
max={1000}
|
||||||
|
step={1}
|
||||||
|
minStepsBetweenThumbs={1}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-zinc-500">
|
||||||
|
{localRange[0]} {localUnit}
|
||||||
|
</span>
|
||||||
|
<span className="text-zinc-500">
|
||||||
|
{localRange[1]} {localUnit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SortFilterProps {
|
||||||
|
sort: "date" | "completion_date" | "size" | undefined;
|
||||||
|
order: "asc" | "desc" | undefined;
|
||||||
|
onChange: (
|
||||||
|
sort: "date" | "completion_date" | "size",
|
||||||
|
order: "asc" | "desc",
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortFilter = ({ sort, order, onChange }: SortFilterProps) => {
|
||||||
|
const currentSort = sort ?? "date";
|
||||||
|
const currentOrder = order ?? "desc";
|
||||||
|
|
||||||
|
const sortOptions = [
|
||||||
|
{ value: "date", label: "Sent Date" },
|
||||||
|
{ value: "completion_date", label: "Downloaded Date" },
|
||||||
|
{ value: "size", label: "File Size" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Sort By</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
value={currentSort}
|
||||||
|
onValueChange={(newSort: typeof currentSort) =>
|
||||||
|
onChange(newSort, currentOrder)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="flex-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{sortOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
onChange(currentSort, currentOrder === "asc" ? "desc" : "asc")
|
||||||
|
}
|
||||||
|
className={cn("h-9 w-9")}
|
||||||
|
>
|
||||||
|
{currentOrder === "asc" ? (
|
||||||
|
<ArrowUpNarrowWide className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ArrowDownNarrowWide className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface FileFiltersProps {
|
interface FileFiltersProps {
|
||||||
telegramId: string;
|
telegramId: string;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
filters: FileFilter;
|
filters: FileFilter;
|
||||||
onFiltersChange: (filters: FileFilter) => void;
|
onFiltersChange: (filters: FileFilter) => void;
|
||||||
columns?: Column[];
|
clearFilters: () => void;
|
||||||
onColumnConfigChange?: (config: Column[]) => void;
|
|
||||||
rowHeight?: RowHeight;
|
|
||||||
setRowHeight?: (e: RowHeight) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableRowHeightSwitch = dynamic(
|
export default function FileFilters({
|
||||||
() =>
|
|
||||||
import("@/components/table-row-height-switch").then(
|
|
||||||
(mod) => mod.TableRowHeightSwitch,
|
|
||||||
),
|
|
||||||
{ ssr: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const statusOptions = {
|
|
||||||
downloadStatus: {
|
|
||||||
text: "Download Status",
|
|
||||||
status: DOWNLOAD_STATUS,
|
|
||||||
},
|
|
||||||
transferStatus: {
|
|
||||||
text: "Transfer Status",
|
|
||||||
status: TRANSFER_STATUS,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FileFilters({
|
|
||||||
telegramId,
|
telegramId,
|
||||||
chatId,
|
chatId,
|
||||||
filters,
|
filters,
|
||||||
onFiltersChange,
|
onFiltersChange,
|
||||||
columns = [],
|
clearFilters,
|
||||||
onColumnConfigChange = () => void 0,
|
|
||||||
rowHeight = "m",
|
|
||||||
setRowHeight = () => void 0,
|
|
||||||
}: FileFiltersProps) {
|
}: FileFiltersProps) {
|
||||||
|
const [localFilters, setLocalFilters] = useState<FileFilter>(filters);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [search, setSearch] = useState(filters.search);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const handleSearchChange = useDebouncedCallback((search: string) => {
|
|
||||||
onFiltersChange({ ...filters, search });
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const handleStatusSelect = (value: string) => {
|
|
||||||
const [group, item] = value.split("||");
|
|
||||||
|
|
||||||
if (group === "downloadStatus") {
|
|
||||||
onFiltersChange({
|
|
||||||
...filters,
|
|
||||||
downloadStatus:
|
|
||||||
item === filters.downloadStatus
|
|
||||||
? undefined
|
|
||||||
: (item as DownloadStatus),
|
|
||||||
});
|
|
||||||
} else if (group === "transferStatus") {
|
|
||||||
onFiltersChange({
|
|
||||||
...filters,
|
|
||||||
transferStatus:
|
|
||||||
item === filters.transferStatus
|
|
||||||
? undefined
|
|
||||||
: (item as TransferStatus),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusDisplayValue = useMemo(() => {
|
|
||||||
if (!filters.downloadStatus && !filters.transferStatus) {
|
|
||||||
return "Filter Status";
|
|
||||||
}
|
|
||||||
const downloadStatus =
|
|
||||||
filters.downloadStatus && DOWNLOAD_STATUS[filters.downloadStatus];
|
|
||||||
const transferStatus =
|
|
||||||
filters.transferStatus && TRANSFER_STATUS[filters.transferStatus];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{downloadStatus && (
|
|
||||||
<div className="flex items-center space-x-1 rounded bg-gray-100 p-1 dark:bg-gray-800">
|
|
||||||
<span className="text-xs">Download</span>
|
|
||||||
<downloadStatus.icon className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{transferStatus && (
|
|
||||||
<div className="flex items-center space-x-1 rounded bg-gray-100 p-1 dark:bg-gray-800">
|
|
||||||
<span className="text-xs">Transfer</span>
|
|
||||||
<transferStatus.icon className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [filters.downloadStatus, filters.transferStatus]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleSearchChange(search);
|
setLocalFilters(filters);
|
||||||
}, [search, handleSearchChange]);
|
}, [filters]);
|
||||||
|
|
||||||
|
const filterCount = Object.values(filters).filter((value) => {
|
||||||
|
if (typeof value === "string") return value !== "";
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (Array.isArray(value)) return value.length > 0;
|
||||||
|
return false;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const handleSearchChange = (search: string) => {
|
||||||
|
setLocalFilters((prev) => ({ ...prev, search }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTypeChange = (type: FileType | "all") => {
|
||||||
|
setLocalFilters((prev) => ({ ...prev, type }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusChange = (
|
||||||
|
downloadStatus?: DownloadStatus,
|
||||||
|
transferStatus?: TransferStatus,
|
||||||
|
) => {
|
||||||
|
setLocalFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
downloadStatus,
|
||||||
|
transferStatus,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateChange = (
|
||||||
|
dateType: "sent" | "downloaded",
|
||||||
|
dateRange: [string, string],
|
||||||
|
) => {
|
||||||
|
setLocalFilters((prev) => ({ ...prev, dateType, dateRange }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeChange = (
|
||||||
|
sizeRange: [number, number],
|
||||||
|
sizeUnit: "KB" | "MB" | "GB",
|
||||||
|
) => {
|
||||||
|
setLocalFilters((prev) => ({ ...prev, sizeRange, sizeUnit }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSortChange = (
|
||||||
|
sort: "date" | "completion_date" | "size",
|
||||||
|
order: "asc" | "desc",
|
||||||
|
) => {
|
||||||
|
setLocalFilters((prev) => ({ ...prev, sort, order }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
onFiltersChange(localFilters);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
clearFilters();
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 flex flex-col flex-wrap gap-2 justify-between md:flex-row">
|
<Drawer
|
||||||
<div className="grid grid-cols-3 gap-4 md:flex md:flex-row">
|
open={open}
|
||||||
<div className="relative col-span-3">
|
onOpenChange={setOpen}
|
||||||
<Input
|
direction={isMobile ? "bottom" : "left"}
|
||||||
placeholder="Search files..."
|
>
|
||||||
value={search}
|
<DrawerTrigger asChild>
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
<Button
|
||||||
className="relative md:w-[300px]"
|
variant="outline"
|
||||||
/>
|
className={cn("relative gap-2", isMobile && "z-50 w-9")}
|
||||||
{search && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute right-2 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full text-gray-500 transition-all duration-200 hover:scale-110 hover:bg-gray-100 hover:text-gray-800"
|
|
||||||
onClick={() => setSearch("")}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FileTypeFilter
|
|
||||||
telegramId={telegramId}
|
|
||||||
chatId={chatId}
|
|
||||||
type={filters.type}
|
|
||||||
onTypeChange={(type) => onFiltersChange({ ...filters, type })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={`${filters.downloadStatus ?? ""}||${filters.transferStatus ?? ""}`}
|
|
||||||
onValueChange={(value) => handleStatusSelect(value)}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger className="col-span-2 w-full md:w-[210px]">
|
<Filter className="h-5 w-5" />
|
||||||
<SelectValue placeholder="Filter Status">
|
{!isMobile && "Filters"}
|
||||||
{statusDisplayValue}
|
{filterCount > 0 && (
|
||||||
</SelectValue>
|
<span className="absolute left-0 top-0 -ml-1 -mt-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-xs text-white">
|
||||||
</SelectTrigger>
|
{filterCount}
|
||||||
<SelectContent>
|
</span>
|
||||||
{Object.entries(statusOptions).map(([groupKey, group]) => (
|
)}
|
||||||
<SelectGroup key={groupKey}>
|
</Button>
|
||||||
<SelectLabel>{group.text}</SelectLabel>
|
</DrawerTrigger>
|
||||||
{Object.entries(group.status).map(([statusKey, item]) => (
|
<DrawerPortal>
|
||||||
<SelectPrimitive.Item
|
<DrawerOverlay className="bg-black/30 dark:bg-black/50" />
|
||||||
key={`${groupKey}||${statusKey}`}
|
<DrawerPrimitive.Content
|
||||||
value={`${groupKey}||${statusKey}`}
|
className={cn(
|
||||||
className={cn(
|
isMobile
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
? "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background"
|
||||||
!isMobile && "focus:bg-accent focus:text-accent-foreground"
|
: "fixed bottom-2 left-2 top-2 z-50 flex w-[380px] outline-none",
|
||||||
)}
|
)}
|
||||||
>
|
style={
|
||||||
{filters[groupKey as keyof FileFilter] === statusKey && (
|
isMobile
|
||||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
? {}
|
||||||
<Check className="h-4 w-4" />
|
: ({ "--initial-transform": "calc(100% + 8px)" } as CSSProperties)
|
||||||
</span>
|
}
|
||||||
)}
|
>
|
||||||
<SelectPrimitive.ItemText>
|
<div className="flex h-full w-full grow flex-col rounded-[16px] bg-white shadow-lg dark:bg-zinc-900">
|
||||||
<div className="flex items-center">
|
<div className="flex-1 p-6">
|
||||||
{item.icon && <item.icon className="mr-2 h-4 w-4" />}
|
<DrawerTitle>
|
||||||
{item.text}
|
<div className="flex items-center justify-between">
|
||||||
</div>
|
<span className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||||
</SelectPrimitive.ItemText>
|
Filters
|
||||||
</SelectPrimitive.Item>
|
</span>
|
||||||
))}
|
<div className="flex items-center space-x-2">
|
||||||
</SelectGroup>
|
<Label
|
||||||
))}
|
htmlFor="offline"
|
||||||
</SelectContent>
|
className="cursor-pointer text-zinc-500"
|
||||||
</Select>
|
>
|
||||||
</div>
|
Offline
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
id="offline"
|
||||||
|
checked={localFilters.offline}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setLocalFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
offline: checked,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DrawerTitle>
|
||||||
|
<DrawerDescription className="mb-3">
|
||||||
|
Default search by Telegram Client, you can choose offline to
|
||||||
|
search by local database.
|
||||||
|
</DrawerDescription>
|
||||||
|
|
||||||
{!isMobile && (
|
<div className="space-y-4 overflow-y-auto px-0.5">
|
||||||
<div className="hidden gap-4 md:flex">
|
<SearchFilter
|
||||||
<TableColumnFilter
|
search={localFilters.search}
|
||||||
columns={columns}
|
onChange={handleSearchChange}
|
||||||
onColumnConfigChange={onColumnConfigChange}
|
/>
|
||||||
/>
|
|
||||||
<TableRowHeightSwitch
|
<FileTypeFilter
|
||||||
rowHeight={rowHeight}
|
offline={localFilters.offline}
|
||||||
setRowHeightAction={setRowHeight}
|
telegramId={telegramId}
|
||||||
/>
|
chatId={chatId}
|
||||||
</div>
|
type={filters.type}
|
||||||
)}
|
onChange={handleTypeChange}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
|
{!localFilters.offline && (
|
||||||
|
<div className="flex items-center justify-between rounded-md border bg-zinc-100 px-2 py-3 dark:bg-zinc-800">
|
||||||
|
<Label htmlFor="notDownload">Filter Not Download</Label>
|
||||||
|
<Switch
|
||||||
|
id="notDownload"
|
||||||
|
checked={localFilters.downloadStatus === "idle"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setLocalFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
downloadStatus: checked ? "idle" : undefined,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
aria-label="Not Download"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{localFilters.offline && (
|
||||||
|
<>
|
||||||
|
<FileStatusFilter
|
||||||
|
downloadStatus={localFilters.downloadStatus}
|
||||||
|
transferStatus={localFilters.transferStatus}
|
||||||
|
onChange={handleStatusChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateFilter
|
||||||
|
dateType={localFilters.dateType}
|
||||||
|
dateRange={localFilters.dateRange}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SizeFilter
|
||||||
|
sizeRange={localFilters.sizeRange}
|
||||||
|
sizeUnit={localFilters.sizeUnit}
|
||||||
|
onChange={handleSizeChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SortFilter
|
||||||
|
sort={localFilters.sort}
|
||||||
|
order={localFilters.order}
|
||||||
|
onChange={handleSortChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DrawerFooter>
|
||||||
|
<Button onClick={handleApply}>Apply Filters</Button>
|
||||||
|
<Button variant="outline" onClick={handleClear}>
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
</DrawerFooter>
|
||||||
|
</div>
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
144
web/src/components/file-status-filter.tsx
Normal file
144
web/src/components/file-status-filter.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { DownloadStatus, TransferStatus } from "@/lib/types";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { DOWNLOAD_STATUS, TRANSFER_STATUS } from "@/components/file-status";
|
||||||
|
import useIsMobile from "@/hooks/use-is-mobile";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
const statusOptions = {
|
||||||
|
downloadStatus: {
|
||||||
|
text: "Download Status",
|
||||||
|
status: DOWNLOAD_STATUS,
|
||||||
|
},
|
||||||
|
transferStatus: {
|
||||||
|
text: "Transfer Status",
|
||||||
|
status: TRANSFER_STATUS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FileStatusFilterProps {
|
||||||
|
downloadStatus: DownloadStatus | undefined;
|
||||||
|
transferStatus: TransferStatus | undefined;
|
||||||
|
onChange: (
|
||||||
|
downloadStatus?: DownloadStatus,
|
||||||
|
transferStatus?: TransferStatus,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FileStatusFilter({
|
||||||
|
downloadStatus,
|
||||||
|
transferStatus,
|
||||||
|
onChange,
|
||||||
|
}: FileStatusFilterProps) {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [localDownloadStatus, setLocalDownloadStatus] = React.useState<
|
||||||
|
DownloadStatus | undefined
|
||||||
|
>(downloadStatus);
|
||||||
|
const [localTransferStatus, setLocalTransferStatus] = React.useState<
|
||||||
|
TransferStatus | undefined
|
||||||
|
>(transferStatus);
|
||||||
|
|
||||||
|
const handleStatusSelect = (value: string) => {
|
||||||
|
const [group, item] = value.split("||");
|
||||||
|
|
||||||
|
let downloadStatus = localDownloadStatus;
|
||||||
|
let transferStatus = localTransferStatus;
|
||||||
|
if (group === "downloadStatus") {
|
||||||
|
const updatedDownloadStatus =
|
||||||
|
localDownloadStatus === item ? undefined : (item as DownloadStatus);
|
||||||
|
setLocalDownloadStatus(updatedDownloadStatus);
|
||||||
|
downloadStatus = updatedDownloadStatus;
|
||||||
|
} else if (group === "transferStatus") {
|
||||||
|
const updatedTransferStatus =
|
||||||
|
localTransferStatus === item ? undefined : (item as TransferStatus);
|
||||||
|
setLocalTransferStatus(updatedTransferStatus);
|
||||||
|
transferStatus = updatedTransferStatus;
|
||||||
|
}
|
||||||
|
onChange(downloadStatus, transferStatus);
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusDisplayValue = useMemo(() => {
|
||||||
|
if (!localDownloadStatus && !localTransferStatus) {
|
||||||
|
return "Filter Status";
|
||||||
|
}
|
||||||
|
const downloadStatus =
|
||||||
|
localDownloadStatus && DOWNLOAD_STATUS[localDownloadStatus];
|
||||||
|
const transferStatus =
|
||||||
|
localTransferStatus && TRANSFER_STATUS[localTransferStatus];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{downloadStatus && (
|
||||||
|
<div className="flex items-center space-x-1 rounded bg-gray-100 p-1 dark:bg-gray-800">
|
||||||
|
<span className="text-xs">Download</span>
|
||||||
|
<downloadStatus.icon className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{transferStatus && (
|
||||||
|
<div className="flex items-center space-x-1 rounded bg-gray-100 p-1 dark:bg-gray-800">
|
||||||
|
<span className="text-xs">Transfer</span>
|
||||||
|
<transferStatus.icon className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [localDownloadStatus, localTransferStatus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<Select
|
||||||
|
value={`${localDownloadStatus ?? ""}||${localTransferStatus ?? ""}`}
|
||||||
|
onValueChange={(value) => handleStatusSelect(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Filter Status">
|
||||||
|
{statusDisplayValue}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.entries(statusOptions).map(([groupKey, group]) => (
|
||||||
|
<SelectGroup key={groupKey}>
|
||||||
|
<SelectLabel>{group.text}</SelectLabel>
|
||||||
|
{Object.entries(group.status).map(([statusKey, item]) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
key={`${groupKey}||${statusKey}`}
|
||||||
|
value={`${groupKey}||${statusKey}`}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
!isMobile && "focus:bg-accent focus:text-accent-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(groupKey === "downloadStatus"
|
||||||
|
? localDownloadStatus
|
||||||
|
: localTransferStatus) === statusKey && (
|
||||||
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<SelectPrimitive.ItemText>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{item.icon && <item.icon className="mr-2 h-4 w-4" />}
|
||||||
|
{item.text}
|
||||||
|
</div>
|
||||||
|
</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,12 +4,14 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Download, LoaderCircle, LoaderPinwheel } from "lucide-react";
|
import { Download, LoaderCircle, LoaderPinwheel } from "lucide-react";
|
||||||
import { useFiles } from "@/hooks/use-files";
|
import { useFiles } from "@/hooks/use-files";
|
||||||
import { FileFilters } from "@/components/file-filters";
|
|
||||||
import {
|
import {
|
||||||
getRowHeightPX,
|
getRowHeightPX,
|
||||||
|
TableRowHeightSwitch,
|
||||||
useRowHeightLocalStorage,
|
useRowHeightLocalStorage,
|
||||||
} from "@/components/table-row-height-switch";
|
} from "@/components/table-row-height-switch";
|
||||||
import { type Column } from "@/components/table-column-filter";
|
import TableColumnFilter, {
|
||||||
|
type Column,
|
||||||
|
} from "@/components/table-column-filter";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import FileNotFount from "@/components/file-not-found";
|
import FileNotFount from "@/components/file-not-found";
|
||||||
import useSWRMutation from "swr/mutation";
|
import useSWRMutation from "swr/mutation";
|
||||||
|
|
@ -18,6 +20,8 @@ import FileRow from "@/components/file-row";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { type TelegramFile } from "@/lib/types";
|
import { type TelegramFile } from "@/lib/types";
|
||||||
import FileViewer from "@/components/file-viewer";
|
import FileViewer from "@/components/file-viewer";
|
||||||
|
import FileFilters from "./file-filters";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS: Column[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -62,9 +66,17 @@ export function FileTable({ accountId, chatId }: FileTableProps) {
|
||||||
"m",
|
"m",
|
||||||
);
|
);
|
||||||
const useFilesProps = useFiles(accountId, chatId);
|
const useFilesProps = useFiles(accountId, chatId);
|
||||||
const { filters, handleFilterChange, isLoading, files, handleLoadMore } =
|
const {
|
||||||
useFilesProps;
|
filters,
|
||||||
const [currentViewFile, setCurrentViewFile] = useState<TelegramFile | undefined>();
|
handleFilterChange,
|
||||||
|
clearFilters,
|
||||||
|
isLoading,
|
||||||
|
files,
|
||||||
|
handleLoadMore,
|
||||||
|
} = useFilesProps;
|
||||||
|
const [currentViewFile, setCurrentViewFile] = useState<
|
||||||
|
TelegramFile | undefined
|
||||||
|
>();
|
||||||
const [viewerOpen, setViewerOpen] = useState(false);
|
const [viewerOpen, setViewerOpen] = useState(false);
|
||||||
const {
|
const {
|
||||||
trigger: startDownloadMultiple,
|
trigger: startDownloadMultiple,
|
||||||
|
|
@ -185,16 +197,30 @@ export function FileTable({ accountId, chatId }: FileTableProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FileFilters
|
<div className="mb-6 flex flex-col flex-wrap justify-between gap-2 md:flex-row">
|
||||||
telegramId={accountId}
|
<div className="flex items-center gap-3">
|
||||||
chatId={chatId}
|
<Badge variant="outline" className="flex h-full bg-accent">
|
||||||
filters={filters}
|
{filters.type.charAt(0).toUpperCase() + filters.type.slice(1)}
|
||||||
onFiltersChange={handleFilterChange}
|
</Badge>
|
||||||
columns={columns}
|
<FileFilters
|
||||||
onColumnConfigChange={setColumns}
|
telegramId={accountId}
|
||||||
rowHeight={rowHeight}
|
chatId={chatId}
|
||||||
setRowHeight={setRowHeight}
|
filters={filters}
|
||||||
/>
|
onFiltersChange={handleFilterChange}
|
||||||
|
clearFilters={clearFilters}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden gap-4 md:flex">
|
||||||
|
<TableColumnFilter
|
||||||
|
columns={columns}
|
||||||
|
onColumnConfigChange={setColumns}
|
||||||
|
/>
|
||||||
|
<TableRowHeightSwitch
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
setRowHeightAction={setRowHeight}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{currentViewFile && (
|
{currentViewFile && (
|
||||||
<FileViewer
|
<FileViewer
|
||||||
open={viewerOpen}
|
open={viewerOpen}
|
||||||
|
|
|
||||||
|
|
@ -8,29 +8,41 @@ import {
|
||||||
import type { FileType } from "@/lib/types";
|
import type { FileType } from "@/lib/types";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Ellipsis } from "lucide-react";
|
import { Ellipsis } from "lucide-react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
interface FileTypeFilterProps {
|
interface FileTypeFilterProps {
|
||||||
|
offline: boolean;
|
||||||
telegramId: string;
|
telegramId: string;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
type: FileType;
|
type: FileType | "all";
|
||||||
onTypeChange: (type: FileType) => void;
|
onChange: (type: FileType | "all") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FileTypeFilter({
|
export default function FileTypeFilter({
|
||||||
|
offline,
|
||||||
telegramId,
|
telegramId,
|
||||||
chatId,
|
chatId,
|
||||||
type,
|
type,
|
||||||
onTypeChange,
|
onChange,
|
||||||
}: FileTypeFilterProps) {
|
}: FileTypeFilterProps) {
|
||||||
|
const [localType, setLocalType] = React.useState<FileType | "all">(type);
|
||||||
const { data: counts, isLoading } = useSWR<Record<FileType, number>>(
|
const { data: counts, isLoading } = useSWR<Record<FileType, number>>(
|
||||||
`/telegram/${telegramId}/chat/${chatId}/files/count`,
|
`/telegram/${telegramId}/chat/${chatId}/files/count`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleTypeChange = (value: FileType | "all") => {
|
||||||
|
setLocalType(value);
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
const FileTypeSelectItem = ({ value }: { value: FileType }) => {
|
const FileTypeSelectItem = ({ value }: { value: FileType }) => {
|
||||||
return (
|
return (
|
||||||
<SelectItem value={value}>
|
<SelectItem value={value}>
|
||||||
<div className="flex w-20 items-center justify-between">
|
<div className="flex items-center gap-5">
|
||||||
<span>{value.charAt(0).toUpperCase() + value.slice(1)}</span>
|
<span className="w-10">
|
||||||
|
{value.charAt(0).toUpperCase() + value.slice(1)}
|
||||||
|
</span>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Ellipsis className="h-4 w-4 animate-pulse" />
|
<Ellipsis className="h-4 w-4 animate-pulse" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -44,20 +56,21 @@ export default function FileTypeFilter({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<div className="space-y-2">
|
||||||
value={type}
|
<Label>Type</Label>
|
||||||
onValueChange={(value) => onTypeChange(value as FileType)}
|
<Select value={localType} onValueChange={handleTypeChange}>
|
||||||
>
|
<SelectTrigger>
|
||||||
<SelectTrigger className="w-full md:w-[150px]">
|
<SelectValue placeholder="File type" />
|
||||||
<SelectValue placeholder="File type" />
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
{offline && <SelectItem value="all">All files</SelectItem>}
|
||||||
<FileTypeSelectItem value="media" />
|
<FileTypeSelectItem value="media" />
|
||||||
<FileTypeSelectItem value="photo" />
|
<FileTypeSelectItem value="photo" />
|
||||||
<FileTypeSelectItem value="video" />
|
<FileTypeSelectItem value="video" />
|
||||||
<FileTypeSelectItem value="audio" />
|
<FileTypeSelectItem value="audio" />
|
||||||
<FileTypeSelectItem value="file" />
|
<FileTypeSelectItem value="file" />
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { FileFilters } from "@/components/file-filters";
|
|
||||||
import { LoaderPinwheel } from "lucide-react";
|
import { LoaderPinwheel } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useFiles } from "@/hooks/use-files";
|
import { useFiles } from "@/hooks/use-files";
|
||||||
|
|
@ -8,6 +7,8 @@ import { cn } from "@/lib/utils";
|
||||||
import FileDrawer from "@/components/mobile/file-drawer";
|
import FileDrawer from "@/components/mobile/file-drawer";
|
||||||
import type { TelegramFile } from "@/lib/types";
|
import type { TelegramFile } from "@/lib/types";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
|
import FileFilters from "@/components/file-filters";
|
||||||
|
import DraggableElement from "@/components/ui/draggable-element";
|
||||||
|
|
||||||
interface FileListProps {
|
interface FileListProps {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|
@ -24,6 +25,7 @@ export default function FileList({ accountId, chatId }: FileListProps) {
|
||||||
const {
|
const {
|
||||||
filters,
|
filters,
|
||||||
handleFilterChange,
|
handleFilterChange,
|
||||||
|
clearFilters,
|
||||||
isLoading,
|
isLoading,
|
||||||
files,
|
files,
|
||||||
hasMore,
|
hasMore,
|
||||||
|
|
@ -67,12 +69,15 @@ export default function FileList({ accountId, chatId }: FileListProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FileFilters
|
<DraggableElement>
|
||||||
telegramId={accountId}
|
<FileFilters
|
||||||
chatId={chatId}
|
telegramId={accountId}
|
||||||
filters={filters}
|
chatId={chatId}
|
||||||
onFiltersChange={handleFilterChange}
|
filters={filters}
|
||||||
/>
|
onFiltersChange={handleFilterChange}
|
||||||
|
clearFilters={clearFilters}
|
||||||
|
/>
|
||||||
|
</DraggableElement>
|
||||||
{currentViewFile && (
|
{currentViewFile && (
|
||||||
<FileDrawer
|
<FileDrawer
|
||||||
open={isDrawerOpen}
|
open={isDrawerOpen}
|
||||||
|
|
|
||||||
77
web/src/components/ui/calendar.tsx
Normal file
77
web/src/components/ui/calendar.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
...props
|
||||||
|
}: CalendarProps) {
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn("p-3", className)}
|
||||||
|
classNames={{
|
||||||
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||||
|
month: "space-y-4",
|
||||||
|
caption: "flex justify-center pt-1 relative items-center",
|
||||||
|
caption_label: "text-sm font-medium",
|
||||||
|
nav: "space-x-1 flex items-center",
|
||||||
|
nav_button: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||||
|
),
|
||||||
|
nav_button_previous: "absolute left-1",
|
||||||
|
nav_button_next: "absolute right-1",
|
||||||
|
table: "w-full border-collapse space-y-1",
|
||||||
|
head_row: "flex",
|
||||||
|
head_cell:
|
||||||
|
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||||
|
row: "flex w-full mt-2",
|
||||||
|
cell: cn(
|
||||||
|
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||||
|
props.mode === "range"
|
||||||
|
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||||
|
: "[&:has([aria-selected])]:rounded-md",
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
buttonVariants({ variant: "ghost" }),
|
||||||
|
"h-8 w-8 p-0 font-normal aria-selected:opacity-100",
|
||||||
|
),
|
||||||
|
day_range_start: "day-range-start",
|
||||||
|
day_range_end: "day-range-end",
|
||||||
|
day_selected:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
|
day_today: "bg-accent text-accent-foreground",
|
||||||
|
day_outside:
|
||||||
|
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||||
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
|
day_range_middle:
|
||||||
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
|
day_hidden: "invisible",
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
IconLeft: ({ className, ...props }) => (
|
||||||
|
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
||||||
|
),
|
||||||
|
IconRight: ({ className, ...props }) => (
|
||||||
|
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Calendar.displayName = "Calendar";
|
||||||
|
|
||||||
|
export { Calendar };
|
||||||
164
web/src/components/ui/draggable-element.tsx
Normal file
164
web/src/components/ui/draggable-element.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { DndContext, type DragEndEvent, useDraggable } from "@dnd-kit/core";
|
||||||
|
import { Portal } from "@radix-ui/react-portal";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface DraggableElementProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
onTap?: () => void;
|
||||||
|
longPressTime?: number; // 长按触发拖拽的时间 (ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DraggableContent = ({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
style,
|
||||||
|
onTap,
|
||||||
|
longPressTime = 300,
|
||||||
|
}: DraggableElementProps & { style: React.CSSProperties }) => {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
||||||
|
useDraggable({
|
||||||
|
id: "draggable-element",
|
||||||
|
});
|
||||||
|
|
||||||
|
const touchStartTime = useRef<number>(0);
|
||||||
|
const touchStartPos = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
const longPressTimer = useRef<NodeJS.Timeout>();
|
||||||
|
const [isLongPress, setIsLongPress] = useState(false);
|
||||||
|
|
||||||
|
const transformStyle = transform
|
||||||
|
? {
|
||||||
|
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const handleTouchStart = (e: React.TouchEvent) => {
|
||||||
|
touchStartTime.current = Date.now();
|
||||||
|
touchStartPos.current = {
|
||||||
|
x: e.touches[0]!.clientX,
|
||||||
|
y: e.touches[0]!.clientY,
|
||||||
|
};
|
||||||
|
|
||||||
|
longPressTimer.current = setTimeout(() => {
|
||||||
|
setIsLongPress(true);
|
||||||
|
}, longPressTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e: React.TouchEvent) => {
|
||||||
|
clearTimeout(longPressTimer.current);
|
||||||
|
|
||||||
|
if (touchStartPos.current) {
|
||||||
|
const touchEndX = e.changedTouches[0]!.clientX;
|
||||||
|
const touchEndY = e.changedTouches[0]!.clientY;
|
||||||
|
const deltaX = Math.abs(touchEndX - touchStartPos.current.x);
|
||||||
|
const deltaY = Math.abs(touchEndY - touchStartPos.current.y);
|
||||||
|
const touchDuration = Date.now() - touchStartTime.current;
|
||||||
|
|
||||||
|
// 如果移动距离小于阈值且不是长按,则视为点击
|
||||||
|
if (
|
||||||
|
deltaX < 5 &&
|
||||||
|
deltaY < 5 &&
|
||||||
|
touchDuration < longPressTime &&
|
||||||
|
!isLongPress
|
||||||
|
) {
|
||||||
|
onTap?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLongPress(false);
|
||||||
|
touchStartPos.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: React.TouchEvent) => {
|
||||||
|
if (touchStartPos.current) {
|
||||||
|
const deltaX = Math.abs(e.touches[0]!.clientX - touchStartPos.current.x);
|
||||||
|
const deltaY = Math.abs(e.touches[0]!.clientY - touchStartPos.current.y);
|
||||||
|
|
||||||
|
// 如果移动距离超过阈值,取消长按计时
|
||||||
|
if (deltaX > 5 || deltaY > 5) {
|
||||||
|
clearTimeout(longPressTimer.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const preventContextMenu = (e: React.MouseEvent | React.TouchEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
{...(isLongPress ? listeners : {})}
|
||||||
|
{...attributes}
|
||||||
|
className={cn(
|
||||||
|
`fixed touch-none select-none`,
|
||||||
|
isDragging && "opacity-80",
|
||||||
|
isLongPress && "cursor-grabbing",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
...transformStyle,
|
||||||
|
touchAction: "none",
|
||||||
|
WebkitUserSelect: "none", // 禁用文本选择
|
||||||
|
WebkitTouchCallout: "none", // 禁用触摸回调
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
onContextMenu={preventContextMenu}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
onTouchMove={handleTouchMove}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`relative ${isLongPress ? "scale-105 transition-transform" : ""}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{isLongPress && (
|
||||||
|
<div className="absolute inset-0 animate-pulse rounded-lg bg-black/5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DraggableElement = (props: DraggableElementProps) => {
|
||||||
|
const [position, setPosition] = useState({
|
||||||
|
x: window.innerWidth - 60,
|
||||||
|
y: window.innerHeight - 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { delta } = event;
|
||||||
|
|
||||||
|
setPosition((prev) => {
|
||||||
|
const newX = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(prev.x + delta.x, window.innerWidth - 40),
|
||||||
|
);
|
||||||
|
const newY = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(prev.y + delta.y, window.innerHeight - 40),
|
||||||
|
);
|
||||||
|
return { x: newX, y: newY };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
top: position.y,
|
||||||
|
left: position.x,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<DndContext onDragEnd={handleDragEnd}>
|
||||||
|
<div id="draggable-content">
|
||||||
|
<DraggableContent {...props} style={style} />
|
||||||
|
</div>
|
||||||
|
</DndContext>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DraggableElement;
|
||||||
44
web/src/components/ui/radio-group.tsx
Normal file
44
web/src/components/ui/radio-group.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
|
import { Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
|
<Circle className="h-3.5 w-3.5 fill-primary" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem };
|
||||||
|
|
@ -25,4 +25,25 @@ const Slider = React.forwardRef<
|
||||||
));
|
));
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Slider };
|
const RangeSlider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="cursor-pointer block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
<SliderPrimitive.Thumb className="cursor-pointer block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
));
|
||||||
|
RangeSlider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Slider, RangeSlider };
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const DEFAULT_FILTERS: FileFilter = {
|
||||||
type: "media",
|
type: "media",
|
||||||
downloadStatus: undefined,
|
downloadStatus: undefined,
|
||||||
transferStatus: undefined,
|
transferStatus: undefined,
|
||||||
|
offline: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
type FileResponse = {
|
type FileResponse = {
|
||||||
|
|
@ -51,6 +52,13 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
...(filters.type && { type: filters.type }),
|
...(filters.type && { type: filters.type }),
|
||||||
...(filters.downloadStatus && { downloadStatus: filters.downloadStatus }),
|
...(filters.downloadStatus && { downloadStatus: filters.downloadStatus }),
|
||||||
...(filters.transferStatus && { transferStatus: filters.transferStatus }),
|
...(filters.transferStatus && { transferStatus: filters.transferStatus }),
|
||||||
|
...(filters.offline && { offline: "true" }),
|
||||||
|
...(filters.dateType && { dateType: filters.dateType }),
|
||||||
|
...(filters.dateRange && { dateRange: filters.dateRange.join(",") }),
|
||||||
|
...(filters.sizeRange && { sizeRange: filters.sizeRange.join(",") }),
|
||||||
|
...(filters.sizeUnit && { sizeUnit: filters.sizeUnit }),
|
||||||
|
...(filters.sort && { sort: filters.sort }),
|
||||||
|
...(filters.order && { order: filters.order }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
|
|
@ -181,10 +189,11 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
|
|
||||||
const handleFilterChange = async (newFilters: FileFilter) => {
|
const handleFilterChange = async (newFilters: FileFilter) => {
|
||||||
if (
|
if (
|
||||||
newFilters.search === filters.search &&
|
Object.keys(newFilters).every(
|
||||||
newFilters.type === filters.type &&
|
(key) =>
|
||||||
newFilters.downloadStatus === filters.downloadStatus &&
|
newFilters[key as keyof FileFilter] ===
|
||||||
newFilters.transferStatus === filters.transferStatus
|
filters[key as keyof FileFilter],
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +206,7 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
filters,
|
filters,
|
||||||
isLoading: debounceLoading,
|
isLoading: debounceLoading,
|
||||||
handleFilterChange,
|
handleFilterChange,
|
||||||
|
clearFilters,
|
||||||
handleLoadMore,
|
handleLoadMore,
|
||||||
hasMore,
|
hasMore,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,16 @@ export type TDFile = {
|
||||||
|
|
||||||
export type FileFilter = {
|
export type FileFilter = {
|
||||||
search: string;
|
search: string;
|
||||||
type: FileType;
|
type: FileType | "all";
|
||||||
downloadStatus?: DownloadStatus;
|
downloadStatus?: DownloadStatus;
|
||||||
transferStatus?: TransferStatus;
|
transferStatus?: TransferStatus;
|
||||||
|
offline: boolean;
|
||||||
|
dateType?: "sent" | "downloaded";
|
||||||
|
dateRange?: [string, string];
|
||||||
|
sizeRange?: [number, number];
|
||||||
|
sizeUnit?: "KB" | "MB" | "GB";
|
||||||
|
sort?: "date" | "completion_date" | "size";
|
||||||
|
order?: "asc" | "desc";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TelegramApiResult = {
|
export type TelegramApiResult = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue