✨ 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();
|
||||
}
|
||||
|
||||
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) {
|
||||
String downloadStatus = filter.get("downloadStatus");
|
||||
if (Arrays.asList("downloading", "paused", "completed", "error").contains(downloadStatus)) {
|
||||
boolean offline = Convert.toBool(filter.get("offline"), false);
|
||||
if (offline) {
|
||||
return DataVerticle.fileRepository.getFiles(chatId, filter)
|
||||
.compose(r -> {
|
||||
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.FileRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
|
@ -83,16 +87,35 @@ public class FileRepositoryImpl implements FileRepository {
|
|||
|
||||
@Override
|
||||
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 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);
|
||||
String type = filter.get("type");
|
||||
int limit = Convert.toInt(filter.get("limit"), 20);
|
||||
|
||||
String whereClause = "chat_id = #{chatId}";
|
||||
Map<String, Object> params = MapUtil.of("chatId", chatId);
|
||||
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)) {
|
||||
whereClause += " AND download_status = #{downloadStatus}";
|
||||
params.put("downloadStatus", downloadStatus);
|
||||
|
|
@ -101,16 +124,34 @@ public class FileRepositoryImpl implements FileRepository {
|
|||
whereClause += " AND transfer_status = #{transferStatus}";
|
||||
params.put("transferStatus", transferStatus);
|
||||
}
|
||||
if (StrUtil.isNotBlank(search)) {
|
||||
whereClause += " AND (file_name LIKE #{search} OR caption LIKE #{search})";
|
||||
params.put("search", "%%" + search + "%%");
|
||||
if (StrUtil.isNotBlank(dateType) && StrUtil.isNotBlank(dateRange)) {
|
||||
String[] dates = dateRange.split(",");
|
||||
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 (Objects.equals(type, "media")) {
|
||||
whereClause += " AND type IN ('photo', 'video')";
|
||||
} else {
|
||||
whereClause += " AND type = #{type}";
|
||||
params.put("type", type);
|
||||
if (StrUtil.isNotBlank(sizeRange) && StrUtil.isNotBlank(sizeUnit)) {
|
||||
String[] sizes = sizeRange.split(",");
|
||||
if (sizes.length == 2) {
|
||||
long minSize = MessyUtils.convertToByte(Convert.toLong(sizes[0]), sizeUnit);
|
||||
long maxSize = MessyUtils.convertToByte(Convert.toLong(sizes[1]), sizeUnit);
|
||||
whereClause += " AND size >= #{minSize} AND size <= #{maxSize}";
|
||||
params.put("minSize", minSize);
|
||||
params.put("maxSize", maxSize);
|
||||
}
|
||||
}
|
||||
String countClause = whereClause;
|
||||
|
|
@ -118,11 +159,16 @@ public class FileRepositoryImpl implements FileRepository {
|
|||
whereClause += " AND message_id < #{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(
|
||||
SqlTemplate
|
||||
.forQuery(pool, """
|
||||
SELECT * FROM file_record WHERE %s ORDER BY message_id desc LIMIT #{limit}
|
||||
""".formatted(whereClause))
|
||||
SELECT * FROM file_record WHERE %s ORDER BY %s LIMIT #{limit}
|
||||
""".formatted(whereClause, orderBy))
|
||||
.mapTo(FileRecord.ROW_MAPPER)
|
||||
.execute(params)
|
||||
.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",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "telegram-files-web",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.2.0",
|
||||
"@dnd-kit/modifiers": "^8.0.0",
|
||||
"@dnd-kit/sortable": "^9.0.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
|
|
@ -20,11 +19,13 @@
|
|||
"@radix-ui/react-hover-card": "^1.1.4",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-portal": "^1.1.4",
|
||||
"@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-separator": "^1.1.1",
|
||||
"@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-tabs": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
|
|
@ -35,7 +36,7 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"framer-motion": "^11.15.0",
|
||||
"geist": "^1.3.0",
|
||||
"input-otp": "^1.4.1",
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
"pretty-bytes": "^6.1.1",
|
||||
"qr-code-styling": "^1.9.1",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-use-websocket": "^4.11.1",
|
||||
|
|
@ -124,19 +126,6 @@
|
|||
"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": {
|
||||
"version": "9.0.0",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
||||
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"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",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
|
|
@ -2434,13 +2766,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot/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"
|
||||
|
|
@ -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": {
|
||||
"version": "1.1.1",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
|
|
@ -7132,6 +7529,20 @@
|
|||
"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": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
|
|
@ -8635,15 +9046,6 @@
|
|||
"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": {
|
||||
"version": "9.0.0",
|
||||
"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",
|
||||
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||
"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",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"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": {
|
||||
|
|
@ -9619,6 +10048,25 @@
|
|||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"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": {
|
||||
|
|
@ -9647,12 +10095,22 @@
|
|||
}
|
||||
},
|
||||
"@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==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
||||
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
||||
"requires": {
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@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": {
|
||||
|
|
@ -9670,6 +10128,16 @@
|
|||
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
||||
"requires": {
|
||||
"@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": {
|
||||
|
|
@ -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": {
|
||||
"version": "1.1.0",
|
||||
"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",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"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": {
|
||||
|
|
@ -9820,23 +10381,23 @@
|
|||
"requires": {
|
||||
"@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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"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.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": {
|
||||
|
|
@ -9914,6 +10475,17 @@
|
|||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "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": {
|
||||
|
|
@ -9972,6 +10544,25 @@
|
|||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "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": {
|
||||
|
|
@ -11024,9 +11615,9 @@
|
|||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.7",
|
||||
|
|
@ -12890,6 +13481,12 @@
|
|||
"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": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.2.0",
|
||||
"@dnd-kit/modifiers": "^8.0.0",
|
||||
"@dnd-kit/sortable": "^9.0.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
|
|
@ -28,11 +27,13 @@
|
|||
"@radix-ui/react-hover-card": "^1.1.4",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-portal": "^1.1.4",
|
||||
"@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-separator": "^1.1.1",
|
||||
"@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-tabs": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"framer-motion": "^11.15.0",
|
||||
"geist": "^1.3.0",
|
||||
"input-otp": "^1.4.1",
|
||||
|
|
@ -56,6 +57,7 @@
|
|||
"pretty-bytes": "^6.1.1",
|
||||
"qr-code-styling": "^1.9.1",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-use-websocket": "^4.11.1",
|
||||
|
|
|
|||
|
|
@ -1,215 +1,559 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
type DownloadStatus,
|
||||
type FileFilter,
|
||||
type TransferStatus,
|
||||
type ChangeEvent,
|
||||
type CSSProperties,
|
||||
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";
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import TableColumnFilter, {
|
||||
type Column,
|
||||
} from "@/components/table-column-filter";
|
||||
import { type RowHeight } from "@/components/table-row-height-switch";
|
||||
import FileTypeFilter from "@/components/file-type-filter";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import * as React from "react";
|
||||
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 { Label } from "@/components/ui/label";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { RangeSlider } from "@/components/ui/slider";
|
||||
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";
|
||||
|
||||
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 {
|
||||
telegramId: string;
|
||||
chatId: string;
|
||||
filters: FileFilter;
|
||||
onFiltersChange: (filters: FileFilter) => void;
|
||||
columns?: Column[];
|
||||
onColumnConfigChange?: (config: Column[]) => void;
|
||||
rowHeight?: RowHeight;
|
||||
setRowHeight?: (e: RowHeight) => void;
|
||||
clearFilters: () => void;
|
||||
}
|
||||
|
||||
const TableRowHeightSwitch = dynamic(
|
||||
() =>
|
||||
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({
|
||||
export default function FileFilters({
|
||||
telegramId,
|
||||
chatId,
|
||||
filters,
|
||||
onFiltersChange,
|
||||
columns = [],
|
||||
onColumnConfigChange = () => void 0,
|
||||
rowHeight = "m",
|
||||
setRowHeight = () => void 0,
|
||||
clearFilters,
|
||||
}: FileFiltersProps) {
|
||||
const [localFilters, setLocalFilters] = useState<FileFilter>(filters);
|
||||
const isMobile = useIsMobile();
|
||||
const [search, setSearch] = useState(filters.search);
|
||||
|
||||
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]);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
handleSearchChange(search);
|
||||
}, [search, handleSearchChange]);
|
||||
setLocalFilters(filters);
|
||||
}, [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 (
|
||||
<div className="mb-6 flex flex-col flex-wrap gap-2 justify-between md:flex-row">
|
||||
<div className="grid grid-cols-3 gap-4 md:flex md:flex-row">
|
||||
<div className="relative col-span-3">
|
||||
<Input
|
||||
placeholder="Search files..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="relative md:w-[300px]"
|
||||
/>
|
||||
{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)}
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
direction={isMobile ? "bottom" : "left"}
|
||||
>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("relative gap-2", isMobile && "z-50 w-9")}
|
||||
>
|
||||
<SelectTrigger className="col-span-2 w-full md:w-[210px]">
|
||||
<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"
|
||||
)}
|
||||
>
|
||||
{filters[groupKey as keyof FileFilter] === 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>
|
||||
<Filter className="h-5 w-5" />
|
||||
{!isMobile && "Filters"}
|
||||
{filterCount > 0 && (
|
||||
<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">
|
||||
{filterCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay className="bg-black/30 dark:bg-black/50" />
|
||||
<DrawerPrimitive.Content
|
||||
className={cn(
|
||||
isMobile
|
||||
? "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background"
|
||||
: "fixed bottom-2 left-2 top-2 z-50 flex w-[380px] outline-none",
|
||||
)}
|
||||
style={
|
||||
isMobile
|
||||
? {}
|
||||
: ({ "--initial-transform": "calc(100% + 8px)" } as CSSProperties)
|
||||
}
|
||||
>
|
||||
<div className="flex h-full w-full grow flex-col rounded-[16px] bg-white shadow-lg dark:bg-zinc-900">
|
||||
<div className="flex-1 p-6">
|
||||
<DrawerTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
Filters
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label
|
||||
htmlFor="offline"
|
||||
className="cursor-pointer text-zinc-500"
|
||||
>
|
||||
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="hidden gap-4 md:flex">
|
||||
<TableColumnFilter
|
||||
columns={columns}
|
||||
onColumnConfigChange={onColumnConfigChange}
|
||||
/>
|
||||
<TableRowHeightSwitch
|
||||
rowHeight={rowHeight}
|
||||
setRowHeightAction={setRowHeight}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4 overflow-y-auto px-0.5">
|
||||
<SearchFilter
|
||||
search={localFilters.search}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
|
||||
<FileTypeFilter
|
||||
offline={localFilters.offline}
|
||||
telegramId={telegramId}
|
||||
chatId={chatId}
|
||||
type={filters.type}
|
||||
onChange={handleTypeChange}
|
||||
/>
|
||||
|
||||
{!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 { Download, LoaderCircle, LoaderPinwheel } from "lucide-react";
|
||||
import { useFiles } from "@/hooks/use-files";
|
||||
import { FileFilters } from "@/components/file-filters";
|
||||
import {
|
||||
getRowHeightPX,
|
||||
TableRowHeightSwitch,
|
||||
useRowHeightLocalStorage,
|
||||
} 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 FileNotFount from "@/components/file-not-found";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
|
|
@ -18,6 +20,8 @@ import FileRow from "@/components/file-row";
|
|||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { type TelegramFile } from "@/lib/types";
|
||||
import FileViewer from "@/components/file-viewer";
|
||||
import FileFilters from "./file-filters";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
|
|
@ -62,9 +66,17 @@ export function FileTable({ accountId, chatId }: FileTableProps) {
|
|||
"m",
|
||||
);
|
||||
const useFilesProps = useFiles(accountId, chatId);
|
||||
const { filters, handleFilterChange, isLoading, files, handleLoadMore } =
|
||||
useFilesProps;
|
||||
const [currentViewFile, setCurrentViewFile] = useState<TelegramFile | undefined>();
|
||||
const {
|
||||
filters,
|
||||
handleFilterChange,
|
||||
clearFilters,
|
||||
isLoading,
|
||||
files,
|
||||
handleLoadMore,
|
||||
} = useFilesProps;
|
||||
const [currentViewFile, setCurrentViewFile] = useState<
|
||||
TelegramFile | undefined
|
||||
>();
|
||||
const [viewerOpen, setViewerOpen] = useState(false);
|
||||
const {
|
||||
trigger: startDownloadMultiple,
|
||||
|
|
@ -185,16 +197,30 @@ export function FileTable({ accountId, chatId }: FileTableProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<FileFilters
|
||||
telegramId={accountId}
|
||||
chatId={chatId}
|
||||
filters={filters}
|
||||
onFiltersChange={handleFilterChange}
|
||||
columns={columns}
|
||||
onColumnConfigChange={setColumns}
|
||||
rowHeight={rowHeight}
|
||||
setRowHeight={setRowHeight}
|
||||
/>
|
||||
<div className="mb-6 flex flex-col flex-wrap justify-between gap-2 md:flex-row">
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="flex h-full bg-accent">
|
||||
{filters.type.charAt(0).toUpperCase() + filters.type.slice(1)}
|
||||
</Badge>
|
||||
<FileFilters
|
||||
telegramId={accountId}
|
||||
chatId={chatId}
|
||||
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 && (
|
||||
<FileViewer
|
||||
open={viewerOpen}
|
||||
|
|
|
|||
|
|
@ -8,29 +8,41 @@ import {
|
|||
import type { FileType } from "@/lib/types";
|
||||
import useSWR from "swr";
|
||||
import { Ellipsis } from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import * as React from "react";
|
||||
|
||||
interface FileTypeFilterProps {
|
||||
offline: boolean;
|
||||
telegramId: string;
|
||||
chatId: string;
|
||||
type: FileType;
|
||||
onTypeChange: (type: FileType) => void;
|
||||
type: FileType | "all";
|
||||
onChange: (type: FileType | "all") => void;
|
||||
}
|
||||
|
||||
export default function FileTypeFilter({
|
||||
offline,
|
||||
telegramId,
|
||||
chatId,
|
||||
type,
|
||||
onTypeChange,
|
||||
onChange,
|
||||
}: FileTypeFilterProps) {
|
||||
const [localType, setLocalType] = React.useState<FileType | "all">(type);
|
||||
const { data: counts, isLoading } = useSWR<Record<FileType, number>>(
|
||||
`/telegram/${telegramId}/chat/${chatId}/files/count`,
|
||||
);
|
||||
|
||||
const handleTypeChange = (value: FileType | "all") => {
|
||||
setLocalType(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const FileTypeSelectItem = ({ value }: { value: FileType }) => {
|
||||
return (
|
||||
<SelectItem value={value}>
|
||||
<div className="flex w-20 items-center justify-between">
|
||||
<span>{value.charAt(0).toUpperCase() + value.slice(1)}</span>
|
||||
<div className="flex items-center gap-5">
|
||||
<span className="w-10">
|
||||
{value.charAt(0).toUpperCase() + value.slice(1)}
|
||||
</span>
|
||||
{isLoading ? (
|
||||
<Ellipsis className="h-4 w-4 animate-pulse" />
|
||||
) : (
|
||||
|
|
@ -44,20 +56,21 @@ export default function FileTypeFilter({
|
|||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={type}
|
||||
onValueChange={(value) => onTypeChange(value as FileType)}
|
||||
>
|
||||
<SelectTrigger className="w-full md:w-[150px]">
|
||||
<SelectValue placeholder="File type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<FileTypeSelectItem value="media" />
|
||||
<FileTypeSelectItem value="photo" />
|
||||
<FileTypeSelectItem value="video" />
|
||||
<FileTypeSelectItem value="audio" />
|
||||
<FileTypeSelectItem value="file" />
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="space-y-2">
|
||||
<Label>Type</Label>
|
||||
<Select value={localType} onValueChange={handleTypeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="File type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{offline && <SelectItem value="all">All files</SelectItem>}
|
||||
<FileTypeSelectItem value="media" />
|
||||
<FileTypeSelectItem value="photo" />
|
||||
<FileTypeSelectItem value="video" />
|
||||
<FileTypeSelectItem value="audio" />
|
||||
<FileTypeSelectItem value="file" />
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { FileFilters } from "@/components/file-filters";
|
||||
import { LoaderPinwheel } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useFiles } from "@/hooks/use-files";
|
||||
|
|
@ -8,6 +7,8 @@ import { cn } from "@/lib/utils";
|
|||
import FileDrawer from "@/components/mobile/file-drawer";
|
||||
import type { TelegramFile } from "@/lib/types";
|
||||
import { isEqual } from "lodash";
|
||||
import FileFilters from "@/components/file-filters";
|
||||
import DraggableElement from "@/components/ui/draggable-element";
|
||||
|
||||
interface FileListProps {
|
||||
accountId: string;
|
||||
|
|
@ -24,6 +25,7 @@ export default function FileList({ accountId, chatId }: FileListProps) {
|
|||
const {
|
||||
filters,
|
||||
handleFilterChange,
|
||||
clearFilters,
|
||||
isLoading,
|
||||
files,
|
||||
hasMore,
|
||||
|
|
@ -67,12 +69,15 @@ export default function FileList({ accountId, chatId }: FileListProps) {
|
|||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<FileFilters
|
||||
telegramId={accountId}
|
||||
chatId={chatId}
|
||||
filters={filters}
|
||||
onFiltersChange={handleFilterChange}
|
||||
/>
|
||||
<DraggableElement>
|
||||
<FileFilters
|
||||
telegramId={accountId}
|
||||
chatId={chatId}
|
||||
filters={filters}
|
||||
onFiltersChange={handleFilterChange}
|
||||
clearFilters={clearFilters}
|
||||
/>
|
||||
</DraggableElement>
|
||||
{currentViewFile && (
|
||||
<FileDrawer
|
||||
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;
|
||||
|
||||
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",
|
||||
downloadStatus: undefined,
|
||||
transferStatus: undefined,
|
||||
offline: false,
|
||||
};
|
||||
|
||||
type FileResponse = {
|
||||
|
|
@ -51,6 +52,13 @@ export function useFiles(accountId: string, chatId: string) {
|
|||
...(filters.type && { type: filters.type }),
|
||||
...(filters.downloadStatus && { downloadStatus: filters.downloadStatus }),
|
||||
...(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) {
|
||||
|
|
@ -181,10 +189,11 @@ export function useFiles(accountId: string, chatId: string) {
|
|||
|
||||
const handleFilterChange = async (newFilters: FileFilter) => {
|
||||
if (
|
||||
newFilters.search === filters.search &&
|
||||
newFilters.type === filters.type &&
|
||||
newFilters.downloadStatus === filters.downloadStatus &&
|
||||
newFilters.transferStatus === filters.transferStatus
|
||||
Object.keys(newFilters).every(
|
||||
(key) =>
|
||||
newFilters[key as keyof FileFilter] ===
|
||||
filters[key as keyof FileFilter],
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -197,6 +206,7 @@ export function useFiles(accountId: string, chatId: string) {
|
|||
filters,
|
||||
isLoading: debounceLoading,
|
||||
handleFilterChange,
|
||||
clearFilters,
|
||||
handleLoadMore,
|
||||
hasMore,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -97,9 +97,16 @@ export type TDFile = {
|
|||
|
||||
export type FileFilter = {
|
||||
search: string;
|
||||
type: FileType;
|
||||
type: FileType | "all";
|
||||
downloadStatus?: DownloadStatus;
|
||||
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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue