A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: fix watch date picker modal

+552 -190
+91 -1
apps/mobile/app/show/[id].tsx
··· 9 9 showsControllerGetUserShowsQueryKey, 10 10 showsControllerMarkShowWatchedMutation, 11 11 showsControllerUnmarkWatchedMutation, 12 + usersControllerGetMySettingsOptions, 12 13 } from "@opnshelf/api"; 13 14 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 14 15 import { Image } from "expo-image"; ··· 30 31 MetadataPills, 31 32 SeasonCard, 32 33 } from "@/components/detail"; 34 + import { WatchDatePickerModal } from "@/components/WatchDatePickerModal"; 33 35 import { borderRadius, spacing } from "@/constants/spacing"; 34 36 import { useTheme } from "@/contexts/theme"; 35 37 import { useToast } from "@/contexts/toast"; ··· 56 58 const queryClient = useQueryClient(); 57 59 58 60 const [_showListModal, setShowListModal] = useState(false); 61 + const [showDateModal, setShowDateModal] = useState(false); 59 62 60 63 const { data: user } = useQuery({ 61 64 ...authControllerMeOptions(), ··· 85 88 enabled: !!user?.did, 86 89 }); 87 90 91 + const { data: userSettings } = useQuery({ 92 + ...usersControllerGetMySettingsOptions(), 93 + enabled: !!user?.did, 94 + }); 95 + 88 96 const listsCount = listsForShow?.filter((l) => l.isInList).length ?? 0; 89 97 const watchedEpisodeCount = history?.length ?? 0; 98 + const is24Hour = userSettings?.timeFormat === "24h"; 90 99 91 100 const showColors = show?.colors || { 92 101 primary: themeColors.primary, ··· 123 132 }); 124 133 }; 125 134 135 + const handleMarkWatchedWithDate = (date: Date) => { 136 + markShowWatchedMutation.mutate({ 137 + body: { showId: id, watchedAt: date.toISOString() }, 138 + }); 139 + }; 140 + 126 141 const unmarkShowWatchedMutation = useMutation({ 127 142 ...showsControllerUnmarkWatchedMutation(), 128 143 onSuccess: () => { ··· 242 257 totalWatches={watchedEpisodeCount} 243 258 onMarkWatched={handleMarkWatched} 244 259 onUnmarkWatched={handleUnmarkWatched} 245 - onShowDatePicker={() => {}} 260 + onShowDatePicker={() => setShowDateModal(true)} 246 261 isMarkingPending={markShowWatchedMutation.isPending} 247 262 isUnmarkingPending={unmarkShowWatchedMutation.isPending} 248 263 listsCount={listsCount} ··· 466 481 )} 467 482 </View> 468 483 </ScrollView> 484 + 485 + <WatchDatePickerModal 486 + visible={showDateModal} 487 + onDismiss={() => setShowDateModal(false)} 488 + onConfirm={handleMarkWatchedWithDate} 489 + isLoading={markShowWatchedMutation.isPending} 490 + is24Hour={is24Hour} 491 + /> 469 492 </SafeAreaView> 470 493 ); 471 494 } ··· 572 595 }, 573 596 crewJob: { 574 597 fontSize: 12, 598 + }, 599 + modalOverlay: { 600 + flex: 1, 601 + backgroundColor: "rgba(0, 0, 0, 0.7)", 602 + justifyContent: "center", 603 + alignItems: "center", 604 + padding: spacing.lg, 605 + }, 606 + modalContent: { 607 + borderRadius: 28, 608 + padding: spacing.lg, 609 + width: "100%", 610 + maxWidth: 340, 611 + }, 612 + modalHeader: { 613 + flexDirection: "row", 614 + justifyContent: "space-between", 615 + alignItems: "center", 616 + marginBottom: spacing.sm, 617 + }, 618 + modalTitle: { 619 + fontSize: 22, 620 + fontWeight: "600", 621 + }, 622 + modalDescription: { 623 + fontSize: 14, 624 + marginBottom: spacing.lg, 625 + }, 626 + dateTimeContainer: { 627 + gap: spacing.sm, 628 + }, 629 + dateTimeButton: { 630 + flex: 1, 631 + flexDirection: "row", 632 + alignItems: "center", 633 + justifyContent: "center", 634 + gap: spacing.sm, 635 + padding: spacing.md, 636 + borderRadius: borderRadius.md, 637 + borderWidth: 1, 638 + }, 639 + dateTimeText: { 640 + fontSize: 14, 641 + fontWeight: "500", 642 + }, 643 + modalButtons: { 644 + flexDirection: "row", 645 + gap: spacing.md, 646 + }, 647 + modalButton: { 648 + flex: 1, 649 + padding: spacing.md, 650 + borderRadius: borderRadius.md, 651 + alignItems: "center", 652 + }, 653 + modalButtonOutline: { 654 + borderWidth: 1, 655 + }, 656 + modalButtonPrimary: {}, 657 + modalButtonText: { 658 + fontSize: 14, 659 + fontWeight: "600", 660 + }, 661 + modalButtonTextPrimary: { 662 + fontSize: 14, 663 + fontWeight: "600", 664 + color: "#fff", 575 665 }, 576 666 });
+9 -183
apps/mobile/app/show/[id]/season/[seasonNumber]/episode/[episodeNumber]/index.tsx
··· 33 33 TouchableOpacity, 34 34 View, 35 35 } from "react-native"; 36 - import { DatePickerModal, TimePickerModal } from "react-native-paper-dates"; 37 36 import { SafeAreaView } from "react-native-safe-area-context"; 38 37 import { AddToListModal } from "@/components/AddToListModal"; 39 38 import { ··· 44 43 MetadataPills, 45 44 } from "@/components/detail"; 46 45 import { Button } from "@/components/ui/Button"; 46 + import { WatchDatePickerModal } from "@/components/WatchDatePickerModal"; 47 47 import { borderRadius, spacing } from "@/constants/spacing"; 48 48 import { useTheme } from "@/contexts/theme"; 49 49 import { useToast } from "@/contexts/toast"; ··· 91 91 const queryClient = useQueryClient(); 92 92 93 93 const [showDateModal, setShowDateModal] = useState(false); 94 - const [showDatePicker, setShowDatePicker] = useState(false); 95 - const [showTimePicker, setShowTimePicker] = useState(false); 96 - const [customDate, setCustomDate] = useState(new Date()); 97 94 const [showAddToListModal, setShowAddToListModal] = useState(false); 98 95 const [showHistoryModal, setShowHistoryModal] = useState(false); 99 96 ··· 269 266 }); 270 267 }; 271 268 272 - const handleMarkWatchedWithDate = () => { 269 + const handleMarkWatchedWithDate = (date: Date) => { 273 270 markMutation.mutate({ 274 271 body: { 275 272 showId: id, 276 273 seasonNumber: Number(seasonNumber), 277 274 episodeNumber: Number(episodeNumber), 278 - watchedAt: customDate.toISOString(), 275 + watchedAt: date.toISOString(), 279 276 }, 280 277 }); 281 278 }; ··· 304 301 }; 305 302 306 303 const handleOpenDateModal = () => { 307 - setCustomDate(new Date()); 308 304 setShowDateModal(true); 309 305 }; 310 306 ··· 559 555 </ScrollView> 560 556 </SafeAreaView> 561 557 562 - <Modal 558 + <WatchDatePickerModal 563 559 visible={showDateModal} 564 - animationType="fade" 565 - transparent={true} 566 - onRequestClose={() => setShowDateModal(false)} 567 - > 568 - <View style={styles.modalOverlay}> 569 - <View 570 - style={[ 571 - styles.modalContent, 572 - { backgroundColor: themeColors.surfaceContainerHigh }, 573 - ]} 574 - > 575 - <View style={styles.modalHeader}> 576 - <Text 577 - style={[styles.modalTitle, { color: themeColors.onSurface }]} 578 - > 579 - Watch Again 580 - </Text> 581 - <Pressable onPress={() => setShowDateModal(false)}> 582 - <Ionicons 583 - name="close" 584 - size={24} 585 - color={themeColors.onSurface} 586 - /> 587 - </Pressable> 588 - </View> 589 - <Text 590 - style={[ 591 - styles.modalDescription, 592 - { color: themeColors.onSurfaceVariant }, 593 - ]} 594 - > 595 - When did you watch this? 596 - </Text> 597 - 598 - <View style={styles.dateTimeContainer}> 599 - <TouchableOpacity 600 - onPress={() => setShowDatePicker(true)} 601 - style={styles.dateTimeButton} 602 - activeOpacity={0.7} 603 - > 604 - <Ionicons 605 - name="calendar-outline" 606 - size={20} 607 - color={themeColors.onSurfaceVariant} 608 - /> 609 - <Text 610 - style={[ 611 - styles.dateTimeText, 612 - { color: themeColors.onSurface }, 613 - ]} 614 - > 615 - {customDate.toLocaleDateString("en-US", { 616 - year: "numeric", 617 - month: "short", 618 - day: "numeric", 619 - })} 620 - </Text> 621 - </TouchableOpacity> 622 - 623 - <TouchableOpacity 624 - onPress={() => setShowTimePicker(true)} 625 - style={styles.dateTimeButton} 626 - activeOpacity={0.7} 627 - > 628 - <Ionicons 629 - name="time-outline" 630 - size={20} 631 - color={themeColors.onSurfaceVariant} 632 - /> 633 - <Text 634 - style={[ 635 - styles.dateTimeText, 636 - { color: themeColors.onSurface }, 637 - ]} 638 - > 639 - {customDate.toLocaleTimeString("en-US", { 640 - hour: "2-digit", 641 - minute: "2-digit", 642 - hour12: !is24Hour, 643 - })} 644 - </Text> 645 - </TouchableOpacity> 646 - </View> 647 - 648 - <DatePickerModal 649 - visible={showDatePicker} 650 - mode="single" 651 - date={customDate} 652 - locale="en" 653 - onDismiss={() => setShowDatePicker(false)} 654 - onConfirm={(params) => { 655 - setShowDatePicker(false); 656 - if (params.date) { 657 - const newDate = new Date(customDate); 658 - newDate.setFullYear(params.date.getFullYear()); 659 - newDate.setMonth(params.date.getMonth()); 660 - newDate.setDate(params.date.getDate()); 661 - setCustomDate(newDate); 662 - setShowTimePicker(true); 663 - } 664 - }} 665 - /> 666 - 667 - <TimePickerModal 668 - visible={showTimePicker} 669 - hours={customDate.getHours()} 670 - minutes={customDate.getMinutes()} 671 - locale="en" 672 - use24HourClock={is24Hour} 673 - onDismiss={() => setShowTimePicker(false)} 674 - onConfirm={(params) => { 675 - const newDate = new Date(customDate); 676 - newDate.setHours(params.hours); 677 - newDate.setMinutes(params.minutes); 678 - setCustomDate(newDate); 679 - setShowTimePicker(false); 680 - }} 681 - /> 682 - 683 - <View style={styles.modalActionsSplit}> 684 - <Button 685 - variant="outlined" 686 - onPress={() => setShowDateModal(false)} 687 - > 688 - <Text 689 - style={[ 690 - styles.modalCancelText, 691 - { color: themeColors.onSurfaceVariant }, 692 - ]} 693 - > 694 - Cancel 695 - </Text> 696 - </Button> 697 - <Button 698 - onPress={handleMarkWatchedWithDate} 699 - isLoading={markMutation.isPending} 700 - style={{ backgroundColor: themeColors.primary }} 701 - > 702 - <Text 703 - style={[ 704 - styles.modalConfirmText, 705 - { color: themeColors.onPrimary }, 706 - ]} 707 - > 708 - Add Watch 709 - </Text> 710 - </Button> 711 - </View> 712 - </View> 713 - </View> 714 - </Modal> 560 + onDismiss={() => setShowDateModal(false)} 561 + onConfirm={handleMarkWatchedWithDate} 562 + isLoading={markMutation.isPending} 563 + is24Hour={is24Hour} 564 + /> 715 565 716 566 <Modal 717 567 visible={showHistoryModal} ··· 956 806 modalDescription: { 957 807 fontSize: 14, 958 808 }, 959 - dateTimeContainer: { 960 - gap: spacing.sm, 961 - }, 962 - dateTimeButton: { 963 - padding: spacing.md, 964 - borderRadius: borderRadius.md, 965 - backgroundColor: "rgba(255, 255, 255, 0.05)", 966 - flexDirection: "row", 967 - alignItems: "center", 968 - gap: spacing.sm, 969 - }, 970 - dateTimeText: { 971 - fontSize: 15, 972 - fontWeight: "500", 973 - }, 974 - modalActionsSplit: { 975 - flexDirection: "row", 976 - gap: spacing.sm, 977 - justifyContent: "space-between", 978 - }, 979 809 modalCancelText: { 980 - fontSize: 14, 981 - fontWeight: "600", 982 - }, 983 - modalConfirmText: { 984 810 fontSize: 14, 985 811 fontWeight: "600", 986 812 },
+95 -1
apps/mobile/app/show/[id]/season/[seasonNumber]/index.tsx
··· 11 11 showsControllerUnmarkWatchedMutation, 12 12 type TmdbSeasonDetailDto, 13 13 type TmdbShowDetailDto, 14 + usersControllerGetMySettingsOptions, 14 15 } from "@opnshelf/api"; 15 16 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 16 17 import { Image } from "expo-image"; ··· 34 35 MetadataPills, 35 36 SeasonNav, 36 37 } from "@/components/detail"; 38 + import { WatchDatePickerModal } from "@/components/WatchDatePickerModal"; 37 39 import { borderRadius, spacing } from "@/constants/spacing"; 38 40 import { useTheme } from "@/contexts/theme"; 39 41 import { useToast } from "@/contexts/toast"; ··· 64 66 const queryClient = useQueryClient(); 65 67 66 68 const [_showListModal, setShowListModal] = useState(false); 69 + const [showDateModal, setShowDateModal] = useState(false); 67 70 68 71 const { data: user } = useQuery({ 69 72 ...authControllerMeOptions(), ··· 100 103 enabled: !!resolvedUserDid, 101 104 }); 102 105 106 + const { data: userSettings } = useQuery({ 107 + ...usersControllerGetMySettingsOptions(), 108 + enabled: !!resolvedUserDid, 109 + }); 110 + 103 111 const listsCount = listsForShow?.filter((l) => l.isInList).length ?? 0; 112 + const is24Hour = userSettings?.timeFormat === "24h"; 104 113 105 114 const showColors = show?.colors || { 106 115 primary: themeColors.primary, ··· 140 149 }); 141 150 }; 142 151 152 + const handleMarkWatchedWithDate = (date: Date) => { 153 + markSeasonWatchedMutation.mutate({ 154 + body: { 155 + showId: id, 156 + seasonNumber: Number(seasonNumber), 157 + watchedAt: date.toISOString(), 158 + }, 159 + }); 160 + }; 161 + 143 162 const unmarkSeasonWatchedMutation = useMutation({ 144 163 ...showsControllerUnmarkWatchedMutation(), 145 164 onSuccess: () => { ··· 255 274 totalWatches={watchedEpisodeCount} 256 275 onMarkWatched={handleMarkWatched} 257 276 onUnmarkWatched={handleUnmarkWatched} 258 - onShowDatePicker={() => {}} 277 + onShowDatePicker={() => setShowDateModal(true)} 259 278 isMarkingPending={markSeasonWatchedMutation.isPending} 260 279 isUnmarkingPending={unmarkSeasonWatchedMutation.isPending} 261 280 listsCount={listsCount} ··· 503 522 )} 504 523 </View> 505 524 </ScrollView> 525 + 526 + <WatchDatePickerModal 527 + visible={showDateModal} 528 + onDismiss={() => setShowDateModal(false)} 529 + onConfirm={handleMarkWatchedWithDate} 530 + isLoading={markSeasonWatchedMutation.isPending} 531 + is24Hour={is24Hour} 532 + /> 506 533 </SafeAreaView> 507 534 ); 508 535 } ··· 609 636 }, 610 637 crewJob: { 611 638 fontSize: 12, 639 + }, 640 + modalOverlay: { 641 + flex: 1, 642 + backgroundColor: "rgba(0, 0, 0, 0.7)", 643 + justifyContent: "center", 644 + alignItems: "center", 645 + padding: spacing.lg, 646 + }, 647 + modalContent: { 648 + borderRadius: 28, 649 + padding: spacing.lg, 650 + width: "100%", 651 + maxWidth: 340, 652 + }, 653 + modalHeader: { 654 + flexDirection: "row", 655 + justifyContent: "space-between", 656 + alignItems: "center", 657 + marginBottom: spacing.sm, 658 + }, 659 + modalTitle: { 660 + fontSize: 22, 661 + fontWeight: "600", 662 + }, 663 + modalDescription: { 664 + fontSize: 14, 665 + marginBottom: spacing.lg, 666 + }, 667 + dateTimeContainer: { 668 + gap: spacing.sm, 669 + }, 670 + dateTimeButton: { 671 + flex: 1, 672 + flexDirection: "row", 673 + alignItems: "center", 674 + justifyContent: "center", 675 + gap: spacing.sm, 676 + padding: spacing.md, 677 + borderRadius: borderRadius.md, 678 + borderWidth: 1, 679 + }, 680 + dateTimeText: { 681 + fontSize: 14, 682 + fontWeight: "500", 683 + }, 684 + modalButtons: { 685 + flexDirection: "row", 686 + gap: spacing.md, 687 + }, 688 + modalButton: { 689 + flex: 1, 690 + padding: spacing.md, 691 + borderRadius: borderRadius.md, 692 + alignItems: "center", 693 + }, 694 + modalButtonOutline: { 695 + borderWidth: 1, 696 + }, 697 + modalButtonPrimary: {}, 698 + modalButtonText: { 699 + fontSize: 14, 700 + fontWeight: "600", 701 + }, 702 + modalButtonTextPrimary: { 703 + fontSize: 14, 704 + fontWeight: "600", 705 + color: "#fff", 612 706 }, 613 707 });
+253
apps/mobile/components/WatchDatePickerModal.tsx
··· 1 + import { Ionicons } from "@expo/vector-icons"; 2 + import { useState } from "react"; 3 + import { 4 + Modal, 5 + Pressable, 6 + StyleSheet, 7 + Text, 8 + TouchableOpacity, 9 + View, 10 + } from "react-native"; 11 + import { DatePickerModal, TimePickerModal } from "react-native-paper-dates"; 12 + import { Button } from "@/components/ui/Button"; 13 + import { borderRadius, spacing } from "@/constants/spacing"; 14 + import { useTheme } from "@/contexts/theme"; 15 + 16 + type WatchDatePickerModalProps = { 17 + visible: boolean; 18 + onDismiss: () => void; 19 + onConfirm: (date: Date) => void; 20 + isLoading?: boolean; 21 + is24Hour?: boolean; 22 + }; 23 + 24 + export function WatchDatePickerModal({ 25 + visible, 26 + onDismiss, 27 + onConfirm, 28 + isLoading = false, 29 + is24Hour = false, 30 + }: WatchDatePickerModalProps) { 31 + const { colors: themeColors } = useTheme(); 32 + const [customDate, setCustomDate] = useState(new Date()); 33 + const [showDatePicker, setShowDatePicker] = useState(false); 34 + const [showTimePicker, setShowTimePicker] = useState(false); 35 + 36 + const handleConfirm = () => { 37 + onConfirm(customDate); 38 + onDismiss(); 39 + }; 40 + 41 + const handleDateConfirm = (params: { date?: Date }) => { 42 + setShowDatePicker(false); 43 + if (params.date) { 44 + const newDate = new Date(customDate); 45 + newDate.setFullYear(params.date.getFullYear()); 46 + newDate.setMonth(params.date.getMonth()); 47 + newDate.setDate(params.date.getDate()); 48 + setCustomDate(newDate); 49 + setShowTimePicker(true); 50 + } 51 + }; 52 + 53 + const handleTimeConfirm = (params: { hours: number; minutes: number }) => { 54 + const newDate = new Date(customDate); 55 + newDate.setHours(params.hours); 56 + newDate.setMinutes(params.minutes); 57 + setCustomDate(newDate); 58 + setShowTimePicker(false); 59 + }; 60 + 61 + return ( 62 + <> 63 + <Modal 64 + visible={visible} 65 + animationType="fade" 66 + transparent={true} 67 + onRequestClose={onDismiss} 68 + > 69 + <View style={styles.modalOverlay}> 70 + <View 71 + style={[ 72 + styles.modalContent, 73 + { backgroundColor: themeColors.surfaceContainerHigh }, 74 + ]} 75 + > 76 + <View style={styles.modalHeader}> 77 + <Text 78 + style={[styles.modalTitle, { color: themeColors.onSurface }]} 79 + > 80 + Select Watch Date 81 + </Text> 82 + <Pressable onPress={onDismiss}> 83 + <Ionicons 84 + name="close" 85 + size={24} 86 + color={themeColors.onSurface} 87 + /> 88 + </Pressable> 89 + </View> 90 + <Text 91 + style={[ 92 + styles.modalDescription, 93 + { color: themeColors.onSurfaceVariant }, 94 + ]} 95 + > 96 + When did you watch this? 97 + </Text> 98 + 99 + <View style={styles.dateTimeContainer}> 100 + <TouchableOpacity 101 + onPress={() => setShowDatePicker(true)} 102 + style={styles.dateTimeButton} 103 + activeOpacity={0.7} 104 + > 105 + <Ionicons 106 + name="calendar-outline" 107 + size={20} 108 + color={themeColors.onSurfaceVariant} 109 + /> 110 + <Text 111 + style={[ 112 + styles.dateTimeText, 113 + { color: themeColors.onSurface }, 114 + ]} 115 + > 116 + {customDate.toLocaleDateString("en-US", { 117 + year: "numeric", 118 + month: "short", 119 + day: "numeric", 120 + })} 121 + </Text> 122 + </TouchableOpacity> 123 + 124 + <TouchableOpacity 125 + onPress={() => setShowTimePicker(true)} 126 + style={styles.dateTimeButton} 127 + activeOpacity={0.7} 128 + > 129 + <Ionicons 130 + name="time-outline" 131 + size={20} 132 + color={themeColors.onSurfaceVariant} 133 + /> 134 + <Text 135 + style={[ 136 + styles.dateTimeText, 137 + { color: themeColors.onSurface }, 138 + ]} 139 + > 140 + {customDate.toLocaleTimeString("en-US", { 141 + hour: "2-digit", 142 + minute: "2-digit", 143 + hour12: !is24Hour, 144 + })} 145 + </Text> 146 + </TouchableOpacity> 147 + </View> 148 + 149 + <DatePickerModal 150 + visible={showDatePicker} 151 + mode="single" 152 + date={customDate} 153 + locale="en" 154 + onDismiss={() => setShowDatePicker(false)} 155 + onConfirm={handleDateConfirm} 156 + /> 157 + 158 + <TimePickerModal 159 + visible={showTimePicker} 160 + hours={customDate.getHours()} 161 + minutes={customDate.getMinutes()} 162 + locale="en" 163 + use24HourClock={is24Hour} 164 + onDismiss={() => setShowTimePicker(false)} 165 + onConfirm={handleTimeConfirm} 166 + /> 167 + 168 + <View style={styles.modalActionsSplit}> 169 + <Button variant="outlined" onPress={onDismiss}> 170 + <Text 171 + style={[ 172 + styles.modalCancelText, 173 + { color: themeColors.onSurfaceVariant }, 174 + ]} 175 + > 176 + Cancel 177 + </Text> 178 + </Button> 179 + <Button 180 + onPress={handleConfirm} 181 + isLoading={isLoading} 182 + style={{ backgroundColor: themeColors.primary }} 183 + > 184 + <Text 185 + style={[ 186 + styles.modalConfirmText, 187 + { color: themeColors.onPrimary }, 188 + ]} 189 + > 190 + Add Watch 191 + </Text> 192 + </Button> 193 + </View> 194 + </View> 195 + </View> 196 + </Modal> 197 + </> 198 + ); 199 + } 200 + 201 + const styles = StyleSheet.create({ 202 + modalOverlay: { 203 + flex: 1, 204 + backgroundColor: "rgba(0, 0, 0, 0.7)", 205 + justifyContent: "center", 206 + padding: spacing.lg, 207 + }, 208 + modalContent: { 209 + borderRadius: borderRadius.lg, 210 + padding: spacing.md, 211 + gap: spacing.md, 212 + }, 213 + modalHeader: { 214 + flexDirection: "row", 215 + justifyContent: "space-between", 216 + alignItems: "center", 217 + }, 218 + modalTitle: { 219 + fontSize: 20, 220 + fontWeight: "700", 221 + }, 222 + modalDescription: { 223 + fontSize: 14, 224 + }, 225 + dateTimeContainer: { 226 + gap: spacing.sm, 227 + }, 228 + dateTimeButton: { 229 + padding: spacing.md, 230 + borderRadius: borderRadius.md, 231 + backgroundColor: "rgba(255, 255, 255, 0.05)", 232 + flexDirection: "row", 233 + alignItems: "center", 234 + gap: spacing.sm, 235 + }, 236 + dateTimeText: { 237 + fontSize: 15, 238 + fontWeight: "500", 239 + }, 240 + modalActionsSplit: { 241 + flexDirection: "row", 242 + gap: spacing.sm, 243 + justifyContent: "space-between", 244 + }, 245 + modalCancelText: { 246 + fontSize: 14, 247 + fontWeight: "600", 248 + }, 249 + modalConfirmText: { 250 + fontSize: 14, 251 + fontWeight: "600", 252 + }, 253 + });
+1 -1
apps/mobile/components/detail/DetailActions.tsx
··· 121 121 ) : ( 122 122 <View style={styles.buttonContent}> 123 123 <Ionicons name="refresh" size={18} color="#f9fafb" /> 124 - <Text style={styles.primaryButtonText}>Watch Again</Text> 124 + <Text style={styles.primaryButtonText}>Select Watch Date</Text> 125 125 </View> 126 126 )} 127 127 </LinearGradient>
+76
apps/web/src/components/DatePickerModal.tsx
··· 3 3 moviesControllerMarkWatchedMutation, 4 4 showsControllerGetShowWatchHistoryQueryKey, 5 5 showsControllerGetUserShowsQueryKey, 6 + showsControllerMarkSeasonWatchedMutation, 7 + showsControllerMarkShowWatchedMutation, 6 8 showsControllerMarkWatchedMutation, 7 9 } from "@opnshelf/api"; 8 10 import { useMutation, useQueryClient } from "@tanstack/react-query"; ··· 30 32 showId: string; 31 33 seasonNumber: string; 32 34 episodeNumber: string; 35 + } 36 + | { 37 + mode: "season"; 38 + showId: string; 39 + seasonNumber: string; 40 + } 41 + | { 42 + mode: "show"; 43 + showId: string; 33 44 } 34 45 ); 35 46 ··· 95 106 toast.error("Failed to update. Please try again."); 96 107 }, 97 108 }); 109 + const markSeasonMutation = useMutation({ 110 + ...showsControllerMarkSeasonWatchedMutation(), 111 + onSuccess: () => { 112 + if (target.mode === "season") { 113 + queryClient.invalidateQueries({ 114 + queryKey: showsControllerGetUserShowsQueryKey({ 115 + path: { userDid: userDid || "" }, 116 + }), 117 + }); 118 + queryClient.invalidateQueries({ 119 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 120 + path: { userDid: userDid || "", showId: target.showId }, 121 + }), 122 + }); 123 + toast.success("Added to your shelf"); 124 + onClose(); 125 + } 126 + }, 127 + onError: () => { 128 + toast.error("Failed to update. Please try again."); 129 + }, 130 + }); 131 + const markShowMutation = useMutation({ 132 + ...showsControllerMarkShowWatchedMutation(), 133 + onSuccess: () => { 134 + if (target.mode === "show") { 135 + queryClient.invalidateQueries({ 136 + queryKey: showsControllerGetUserShowsQueryKey({ 137 + path: { userDid: userDid || "" }, 138 + }), 139 + }); 140 + queryClient.invalidateQueries({ 141 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 142 + path: { userDid: userDid || "", showId: target.showId }, 143 + }), 144 + }); 145 + toast.success("Added to your shelf"); 146 + onClose(); 147 + } 148 + }, 149 + onError: () => { 150 + toast.error("Failed to update. Please try again."); 151 + }, 152 + }); 98 153 99 154 const handleSubmit = () => { 100 155 if (!customDate) return; ··· 118 173 showId: target.showId, 119 174 seasonNumber: Number(target.seasonNumber), 120 175 episodeNumber: Number(target.episodeNumber), 176 + watchedAt: dateTime.toISOString(), 177 + }, 178 + }); 179 + return; 180 + } 181 + 182 + if (target.mode === "season") { 183 + markSeasonMutation.mutate({ 184 + body: { 185 + showId: target.showId, 186 + seasonNumber: Number(target.seasonNumber), 187 + watchedAt: dateTime.toISOString(), 188 + }, 189 + }); 190 + return; 191 + } 192 + 193 + if (target.mode === "show") { 194 + markShowMutation.mutate({ 195 + body: { 196 + showId: target.showId, 121 197 watchedAt: dateTime.toISOString(), 122 198 }, 123 199 });
+1 -1
apps/web/src/components/detail/DetailActions.tsx
··· 109 109 <AddToShelfButton 110 110 onClick={onMarkWatched} 111 111 isPending={isMarkingPending} 112 - label="Watch Again" 112 + label="Select Watch Date" 113 113 icon={<RotateCcw className="w-4 h-4" />} 114 114 colors={colors} 115 115 size="compact"
+1 -1
apps/web/src/routes/shows.$showId.$title.seasons.$seasonNumber.episodes.$episodeNumber.tsx
··· 447 447 seasonNumber={seasonNumber} 448 448 episodeNumber={episodeNumber} 449 449 userDid={user?.did} 450 - modalTitle="Watch Again" 450 + modalTitle="Select Watch Date" 451 451 /> 452 452 453 453 {user && (
+13 -1
apps/web/src/routes/shows.$showId.$title.seasons.$seasonNumber.tsx
··· 24 24 import { AddToListModal } from "@/components/AddToListModal"; 25 25 import { CastSection } from "@/components/CastSection"; 26 26 import { CrewSection } from "@/components/CrewSection"; 27 + import { DatePickerModal } from "@/components/DatePickerModal"; 27 28 import { 28 29 type ColorTheme, 29 30 DetailActions, ··· 84 85 const { seedColor } = useTheme(); 85 86 86 87 const [showListModal, setShowListModal] = useState(false); 88 + const [showDateModal, setShowDateModal] = useState(false); 87 89 88 90 const { data: user } = useQuery({ 89 91 ...authControllerMeOptions(), ··· 257 259 totalWatches={watchedEpisodeCount} 258 260 onMarkWatched={handleMarkWatched} 259 261 onUnmarkWatched={handleUnmarkWatched} 260 - onShowDatePicker={() => {}} 262 + onShowDatePicker={() => setShowDateModal(true)} 261 263 isMarkingPending={markSeasonWatchedMutation.isPending} 262 264 isUnmarkingPending={unmarkSeasonWatchedMutation.isPending} 263 265 listsCount={listsCount} ··· 340 342 user={user} 341 343 /> 342 344 )} 345 + 346 + <DatePickerModal 347 + open={showDateModal} 348 + onClose={() => setShowDateModal(false)} 349 + mode="season" 350 + showId={showId} 351 + seasonNumber={seasonNumber} 352 + userDid={user?.did} 353 + modalTitle="Select Watch Date" 354 + /> 343 355 </div> 344 356 ); 345 357 }
+12 -1
apps/web/src/routes/shows.$showId.$title.tsx
··· 22 22 import { AddToListModal } from "@/components/AddToListModal"; 23 23 import { CastSection } from "@/components/CastSection"; 24 24 import { CrewSection } from "@/components/CrewSection"; 25 + import { DatePickerModal } from "@/components/DatePickerModal"; 25 26 import { 26 27 type ColorTheme, 27 28 DetailActions, ··· 70 71 const { seedColor } = useTheme(); 71 72 72 73 const [showListModal, setShowListModal] = useState(false); 74 + const [showDateModal, setShowDateModal] = useState(false); 73 75 74 76 const { data: user } = useQuery({ 75 77 ...authControllerMeOptions(), ··· 225 227 totalWatches={watchedEpisodeCount} 226 228 onMarkWatched={handleMarkWatched} 227 229 onUnmarkWatched={handleUnmarkWatched} 228 - onShowDatePicker={() => {}} 230 + onShowDatePicker={() => setShowDateModal(true)} 229 231 isMarkingPending={markShowWatchedMutation.isPending} 230 232 isUnmarkingPending={unmarkShowWatchedMutation.isPending} 231 233 listsCount={listsCount} ··· 306 308 user={user} 307 309 /> 308 310 )} 311 + 312 + <DatePickerModal 313 + open={showDateModal} 314 + onClose={() => setShowDateModal(false)} 315 + mode="show" 316 + showId={showId} 317 + userDid={user?.did} 318 + modalTitle="Select Watch Date" 319 + /> 309 320 </div> 310 321 ); 311 322 }