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.

feat: improved calendar view

+745 -354
+11 -8
apps/mobile/app/_layout.tsx
··· 3 3 import { StatusBar } from "expo-status-bar"; 4 4 import { useEffect, useState } from "react"; 5 5 import { DevToolsBubble } from "react-native-react-query-devtools"; 6 + import { PaperProvider, MD3DarkTheme } from "react-native-paper"; 6 7 import { SafeAreaProvider } from "react-native-safe-area-context"; 7 8 import { LoadingScreen } from "@/components/LoadingScreen"; 8 9 import { M3SnackbarProvider } from "@/components/ui/m3/M3Snackbar"; ··· 83 84 return ( 84 85 <SafeAreaProvider> 85 86 <QueryClientProvider client={queryClient}> 86 - <ThemeProvider> 87 - <AuthProvider> 88 - <LocaleInitializer> 89 - <AppContent /> 90 - </LocaleInitializer> 91 - </AuthProvider> 92 - </ThemeProvider> 93 - <DevToolsBubble queryClient={queryClient} /> 87 + <PaperProvider theme={MD3DarkTheme}> 88 + <ThemeProvider> 89 + <AuthProvider> 90 + <LocaleInitializer> 91 + <AppContent /> 92 + </LocaleInitializer> 93 + </AuthProvider> 94 + </ThemeProvider> 95 + </PaperProvider> 96 + {__DEV__ &&<DevToolsBubble queryClient={queryClient} />} 94 97 </QueryClientProvider> 95 98 </SafeAreaProvider> 96 99 );
+96 -84
apps/mobile/app/movie/[id].tsx
··· 379 379 {user ? ( 380 380 !isWatched ? ( 381 381 <> 382 - <TouchableOpacity 383 - onPress={handleMarkWatched} 384 - disabled={isPending} 385 - style={[ 386 - styles.primaryButton, 387 - { opacity: isPending ? 0.7 : 1 }, 388 - ]} 389 - activeOpacity={0.8} 390 - > 391 - <LinearGradient 392 - colors={[ 393 - movieColors.primary || "#8b5cf6", 394 - movieColors.secondary || "#6366f1", 382 + <View style={styles.primaryButtonRow}> 383 + <TouchableOpacity 384 + onPress={handleMarkWatched} 385 + disabled={isPending} 386 + style={[ 387 + styles.primaryButton, 388 + { flex: 1, opacity: isPending ? 0.7 : 1 }, 395 389 ]} 396 - start={{ x: 0, y: 0 }} 397 - end={{ x: 1, y: 1 }} 398 - style={styles.gradientButton} 390 + activeOpacity={0.8} 399 391 > 400 - {isPending ? ( 401 - <View style={styles.buttonContent}> 402 - <ActivityIndicator color="#f9fafb" /> 403 - <Text style={styles.buttonText}>Loading</Text> 404 - </View> 405 - ) : ( 406 - <View style={styles.buttonContent}> 407 - <Ionicons name="add" size={20} color="#f9fafb" /> 408 - <Text style={styles.buttonText}>Add to Shelf</Text> 409 - </View> 410 - )} 411 - </LinearGradient> 412 - </TouchableOpacity> 392 + <LinearGradient 393 + colors={[ 394 + movieColors.primary || "#8b5cf6", 395 + movieColors.secondary || "#6366f1", 396 + ]} 397 + start={{ x: 0, y: 0 }} 398 + end={{ x: 1, y: 1 }} 399 + style={styles.gradientButton} 400 + > 401 + {isPending ? ( 402 + <View style={styles.buttonContent}> 403 + <ActivityIndicator color="#f9fafb" /> 404 + <Text style={styles.buttonText}>Loading</Text> 405 + </View> 406 + ) : ( 407 + <View style={styles.buttonContent}> 408 + <Ionicons name="add" size={20} color="#1f2937" /> 409 + <Text style={styles.buttonText}>Add to Shelf</Text> 410 + </View> 411 + )} 412 + </LinearGradient> 413 + </TouchableOpacity> 413 414 414 - <TouchableOpacity 415 - onPress={_openDateModal} 416 - style={styles.secondaryButton} 417 - activeOpacity={0.8} 418 - > 419 - <View style={styles.buttonContent}> 415 + <TouchableOpacity 416 + onPress={_openDateModal} 417 + style={styles.calendarButton} 418 + activeOpacity={0.8} 419 + > 420 420 <Ionicons 421 421 name="calendar-outline" 422 - size={18} 422 + size={22} 423 423 color="#9ca3af" 424 424 /> 425 - <Text style={styles.secondaryButtonText}> 426 - Watch on different date 427 - </Text> 428 - </View> 429 - </TouchableOpacity> 425 + </TouchableOpacity> 426 + </View> 430 427 431 428 <TouchableOpacity 432 429 onPress={() => setShowAddToListModal(true)} ··· 474 471 </> 475 472 ) : ( 476 473 <> 477 - <TouchableOpacity 478 - onPress={handleMarkWatched} 479 - disabled={isPending} 480 - style={[ 481 - styles.primaryButton, 482 - { opacity: isPending ? 0.7 : 1 }, 483 - ]} 484 - activeOpacity={0.8} 485 - > 486 - <LinearGradient 487 - colors={[ 488 - movieColors.primary || "#8b5cf6", 489 - movieColors.secondary || "#6366f1", 474 + <View style={styles.primaryButtonRow}> 475 + <TouchableOpacity 476 + onPress={handleMarkWatched} 477 + disabled={isPending} 478 + style={[ 479 + styles.primaryButton, 480 + { flex: 1, opacity: isPending ? 0.7 : 1 }, 490 481 ]} 491 - start={{ x: 0, y: 0 }} 492 - end={{ x: 1, y: 1 }} 493 - style={styles.gradientButton} 482 + activeOpacity={0.8} 494 483 > 495 - {isPending ? ( 496 - <View style={styles.buttonContent}> 497 - <ActivityIndicator color="#f9fafb" /> 498 - <Text style={styles.buttonText}>Loading</Text> 499 - </View> 500 - ) : ( 501 - <View style={styles.buttonContent}> 502 - <Ionicons name="refresh" size={20} color="#f9fafb" /> 503 - <Text style={styles.buttonText}>Watch Now</Text> 504 - </View> 505 - )} 506 - </LinearGradient> 507 - </TouchableOpacity> 484 + <LinearGradient 485 + colors={[ 486 + movieColors.primary || "#8b5cf6", 487 + movieColors.secondary || "#6366f1", 488 + ]} 489 + start={{ x: 0, y: 0 }} 490 + end={{ x: 1, y: 1 }} 491 + style={styles.gradientButton} 492 + > 493 + {isPending ? ( 494 + <View style={styles.buttonContent}> 495 + <ActivityIndicator color="#f9fafb" /> 496 + <Text style={styles.buttonText}>Loading</Text> 497 + </View> 498 + ) : ( 499 + <View style={styles.buttonContent}> 500 + <Ionicons 501 + name="refresh" 502 + size={20} 503 + color="#1f2937" 504 + /> 505 + <Text style={styles.buttonText}>Watch Now</Text> 506 + </View> 507 + )} 508 + </LinearGradient> 509 + </TouchableOpacity> 508 510 509 - <TouchableOpacity 510 - onPress={_openDateModal} 511 - style={styles.secondaryButton} 512 - activeOpacity={0.8} 513 - > 514 - <View style={styles.buttonContent}> 511 + <TouchableOpacity 512 + onPress={_openDateModal} 513 + style={styles.calendarButton} 514 + activeOpacity={0.8} 515 + > 515 516 <Ionicons 516 517 name="calendar-outline" 517 - size={18} 518 + size={22} 518 519 color="#9ca3af" 519 520 /> 520 - <Text style={styles.secondaryButtonText}> 521 - Watch on different date 522 - </Text> 523 - </View> 524 - </TouchableOpacity> 521 + </TouchableOpacity> 522 + </View> 525 523 526 524 <TouchableOpacity 527 525 onPress={() => setShowAddToListModal(true)} ··· 829 827 <View style={styles.modalOverlay}> 830 828 <View style={styles.modalContent}> 831 829 <View style={styles.modalHeader}> 832 - <Text style={styles.modalTitle}>Watch Again</Text> 830 + <Text style={styles.modalTitle}>Watch movie</Text> 833 831 <Pressable onPress={() => setShowDateModal(false)}> 834 832 <Ionicons name="close" size={24} color={colors.onSurface} /> 835 833 </Pressable> ··· 1105 1103 actionsContainer: { 1106 1104 gap: 12, 1107 1105 marginBottom: 24, 1106 + }, 1107 + primaryButtonRow: { 1108 + flexDirection: "row", 1109 + gap: 12, 1110 + alignItems: "stretch", 1108 1111 }, 1109 1112 primaryButton: { 1110 1113 borderRadius: 12, 1111 1114 overflow: "hidden", 1112 1115 }, 1116 + calendarButton: { 1117 + borderRadius: 12, 1118 + paddingVertical: 16, 1119 + paddingHorizontal: 16, 1120 + alignItems: "center", 1121 + justifyContent: "center", 1122 + borderWidth: 1, 1123 + borderColor: "#374151", 1124 + }, 1113 1125 gradientButton: { 1114 1126 paddingVertical: 16, 1115 1127 paddingHorizontal: 24, ··· 1132 1144 gap: 8, 1133 1145 }, 1134 1146 buttonText: { 1135 - color: "#f9fafb", 1147 + color: "#1f2937", 1136 1148 fontSize: 18, 1137 1149 fontWeight: "600", 1138 1150 },
+67 -58
apps/mobile/app/show/[id]/season/[seasonNumber]/episode/[episodeNumber]/index.tsx
··· 534 534 <View style={styles.actions}> 535 535 {user ? ( 536 536 <> 537 - <TouchableOpacity 538 - onPress={handleMarkWatched} 539 - disabled={isPending} 540 - activeOpacity={0.8} 541 - style={{ opacity: isPending ? 0.7 : 1 }} 542 - > 543 - <LinearGradient 544 - colors={[ 545 - showColors.primary || colors.primary, 546 - showColors.secondary || colors.primary, 547 - ]} 548 - start={{ x: 0, y: 0 }} 549 - end={{ x: 1, y: 1 }} 550 - style={styles.primaryAction} 537 + <View style={styles.primaryActionRow}> 538 + <TouchableOpacity 539 + onPress={handleMarkWatched} 540 + disabled={isPending} 541 + activeOpacity={0.8} 542 + style={{ flex: 1, opacity: isPending ? 0.7 : 1 }} 551 543 > 552 - {isPending ? ( 553 - <ActivityIndicator 554 - size="small" 555 - color={colors.onPrimary} 556 - /> 557 - ) : ( 558 - <> 559 - <Ionicons 560 - name={isWatchedEpisode ? "refresh" : "add"} 561 - size={18} 544 + <LinearGradient 545 + colors={[ 546 + showColors.primary || colors.primary, 547 + showColors.secondary || colors.primary, 548 + ]} 549 + start={{ x: 0, y: 0 }} 550 + end={{ x: 1, y: 1 }} 551 + style={styles.primaryAction} 552 + > 553 + {isPending ? ( 554 + <ActivityIndicator 555 + size="small" 562 556 color={colors.onPrimary} 563 557 /> 564 - <Text 565 - style={[ 566 - styles.primaryActionText, 567 - { color: colors.onPrimary }, 568 - ]} 569 - > 570 - {isWatchedEpisode ? "Watch Again" : "Add to Shelf"} 571 - </Text> 572 - </> 573 - )} 574 - </LinearGradient> 575 - </TouchableOpacity> 558 + ) : ( 559 + <> 560 + <Ionicons 561 + name={isWatchedEpisode ? "refresh" : "add"} 562 + size={18} 563 + color={colors.onPrimary} 564 + /> 565 + <Text 566 + style={[ 567 + styles.primaryActionText, 568 + { color: colors.onPrimary }, 569 + ]} 570 + > 571 + {isWatchedEpisode 572 + ? "Watch Again" 573 + : "Add to Shelf"} 574 + </Text> 575 + </> 576 + )} 577 + </LinearGradient> 578 + </TouchableOpacity> 576 579 577 - <TouchableOpacity 578 - onPress={handleOpenDateModal} 579 - activeOpacity={0.8} 580 - style={[ 581 - styles.secondaryAction, 582 - { 583 - backgroundColor: colors.surfaceContainer, 584 - borderColor: colors.outline, 585 - }, 586 - ]} 587 - > 588 - <Ionicons 589 - name="calendar-outline" 590 - size={18} 591 - color={colors.onSurfaceVariant} 592 - /> 593 - <Text 580 + <TouchableOpacity 581 + onPress={handleOpenDateModal} 582 + activeOpacity={0.8} 594 583 style={[ 595 - styles.secondaryActionText, 596 - { color: colors.onSurfaceVariant }, 584 + styles.calendarAction, 585 + { 586 + backgroundColor: colors.surfaceContainer, 587 + borderColor: colors.outline, 588 + }, 597 589 ]} 598 590 > 599 - Watch on different date 600 - </Text> 601 - </TouchableOpacity> 591 + <Ionicons 592 + name="calendar-outline" 593 + size={22} 594 + color={colors.onSurfaceVariant} 595 + /> 596 + </TouchableOpacity> 597 + </View> 602 598 603 599 <TouchableOpacity 604 600 onPress={() => setShowAddToListModal(true)} ··· 1265 1261 actions: { 1266 1262 gap: spacing.sm, 1267 1263 }, 1264 + primaryActionRow: { 1265 + flexDirection: "row", 1266 + gap: spacing.sm, 1267 + alignItems: "stretch", 1268 + }, 1268 1269 primaryAction: { 1269 1270 borderRadius: borderRadius.md, 1270 1271 paddingVertical: 14, ··· 1291 1292 secondaryActionText: { 1292 1293 fontSize: 15, 1293 1294 fontWeight: "500", 1295 + }, 1296 + calendarAction: { 1297 + borderRadius: borderRadius.md, 1298 + borderWidth: 1, 1299 + paddingVertical: 14, 1300 + paddingHorizontal: 14, 1301 + alignItems: "center", 1302 + justifyContent: "center", 1294 1303 }, 1295 1304 watchedCard: { 1296 1305 marginTop: spacing.sm,
+1 -1
apps/mobile/contexts/auth.tsx
··· 89 89 }, [isUserError, queryClient, userError]); 90 90 91 91 const login = useCallback(async (handle?: string) => { 92 - const loginUrl = getLoginUrl(handle); 92 + const loginUrl = getLoginUrl(handle, undefined, "mobile"); 93 93 const result = await WebBrowser.openAuthSessionAsync( 94 94 loginUrl, 95 95 "opnshelf://auth/callback"
+1
apps/mobile/package.json
··· 45 45 "react-dom": "19.1.0", 46 46 "react-native": "0.81.5", 47 47 "react-native-gesture-handler": "~2.28.0", 48 + "react-native-paper": "^5.15.0", 48 49 "react-native-paper-dates": "^0.23.3", 49 50 "react-native-react-query-devtools": "^1.5.1", 50 51 "react-native-reanimated": "~4.1.1",
+7 -7
apps/web/src/components/AddToListModal.tsx
··· 98 98 99 99 return ( 100 100 <Dialog open={open} onOpenChange={onOpenChange}> 101 - <DialogContent className="bg-[var(--md-sys-color-surface-container-high)] border-[var(--md-sys-color-outline)] text-[var(--md-sys-color-on-surface)] max-w-md rounded-[1.75rem]"> 101 + <DialogContent className="bg-(--md-sys-color-surface-container-high) border-(--md-sys-color-outline) text-(--md-sys-color-on-surface) max-w-md rounded-[1.75rem]"> 102 102 <DialogHeader> 103 - <DialogTitle className="text-[var(--md-sys-color-on-surface)]"> 103 + <DialogTitle className="text-(--md-sys-color-on-surface)"> 104 104 Manage Lists 105 105 </DialogTitle> 106 - <DialogDescription className="text-[var(--md-sys-color-on-surface-variant)]"> 106 + <DialogDescription className="text-(--md-sys-color-on-surface-variant)"> 107 107 Add or remove &quot;{mediaTitle}&quot; from your lists 108 108 </DialogDescription> 109 109 </DialogHeader> 110 110 <ScrollArea className="max-h-[300px]"> 111 111 {isLoading && ( 112 112 <div className="flex items-center justify-center py-8"> 113 - <Loader2 className="w-6 h-6 animate-spin text-[var(--md-sys-color-primary)]" /> 113 + <Loader2 className="w-6 h-6 animate-spin text-(--md-sys-color-primary)" /> 114 114 </div> 115 115 )} 116 116 {listsForMovie && ( ··· 131 131 variant="outlined" 132 132 className={`w-full justify-between py-6 ${ 133 133 isInList 134 - ? "bg-[var(--md-sys-color-secondary-container)] border-[var(--md-sys-color-secondary)] text-[var(--md-sys-color-on-secondary-container)] hover:bg-[var(--md-sys-color-secondary-container)]/80" 135 - : "bg-transparent border-[var(--md-sys-color-outline)] text-[var(--md-sys-color-on-surface-variant)] hover:bg-[var(--md-sys-color-surface-container-high)]" 134 + ? "bg-(--md-sys-color-secondary-container) border-(--md-sys-color-secondary) text-(--md-sys-color-on-secondary-container) hover:bg-(--md-sys-color-secondary-container)/80" 135 + : "bg-transparent border-(--md-sys-color-outline) text-(--md-sys-color-on-surface-variant) hover:bg-(--md-sys-color-surface-container-high)" 136 136 }`} 137 137 onClick={() => handleToggleList(list.listSlug, isInList)} 138 138 disabled={isPending} ··· 140 140 <span className="flex items-center gap-2"> 141 141 <span>{list.listName}</span> 142 142 {list.isDefault && ( 143 - <span className="text-xs text-[var(--md-sys-color-on-secondary-container)]/70"> 143 + <span className="text-xs text-(--md-sys-color-on-secondary-container)/70"> 144 144 Default 145 145 </span> 146 146 )}
+4 -4
apps/web/src/components/ConfirmDialog.tsx
··· 38 38 39 39 return ( 40 40 <Dialog open={open} onOpenChange={onOpenChange}> 41 - <DialogContent className="bg-[var(--md-sys-color-surface-container-high)] border-[var(--md-sys-color-outline)] text-[var(--md-sys-color-on-surface)] rounded-[1.75rem]"> 41 + <DialogContent className="bg-(--md-sys-color-surface-container-high) border-(--md-sys-color-outline) text-(--md-sys-color-on-surface) rounded-[1.75rem]"> 42 42 <DialogHeader> 43 - <DialogTitle className="text-[var(--md-sys-color-on-surface)]"> 43 + <DialogTitle className="text-(--md-sys-color-on-surface)"> 44 44 {title} 45 45 </DialogTitle> 46 - <DialogDescription className="text-[var(--md-sys-color-on-surface-variant)]"> 46 + <DialogDescription className="text-(--md-sys-color-on-surface-variant)"> 47 47 {description} 48 48 </DialogDescription> 49 49 </DialogHeader> ··· 52 52 variant="outlined" 53 53 onClick={() => onOpenChange(false)} 54 54 disabled={isLoading} 55 - className="border-[var(--md-sys-color-outline)] text-[var(--md-sys-color-on-surface)]" 55 + className="border-(--md-sys-color-outline) text-(--md-sys-color-on-surface)" 56 56 > 57 57 {cancelText} 58 58 </M3Button>
+1 -1
apps/web/src/components/CreateListDialog.tsx
··· 114 114 onChange={(e) => setDescription(e.target.value)} 115 115 maxLength={500} 116 116 rows={3} 117 - className="bg-[var(--md-sys-color-surface-container-highest)] border-[var(--md-sys-color-outline)] text-[var(--md-sys-color-on-surface)] placeholder:text-[var(--md-sys-color-on-surface-variant)]" 117 + className="bg-(--md-sys-color-surface-container-highest) border-(--md-sys-color-outline) text-(--md-sys-color-on-surface) placeholder:text-(--md-sys-color-on-surface-variant)" 118 118 /> 119 119 </div> 120 120 <div className="flex justify-end gap-2">
+16 -56
apps/web/src/components/DatePickerModal.tsx
··· 7 7 } from "@opnshelf/api"; 8 8 import { useMutation, useQueryClient } from "@tanstack/react-query"; 9 9 import { format } from "date-fns"; 10 - import { Calendar, X } from "lucide-react"; 10 + import { X } from "lucide-react"; 11 11 import { useEffect, useState } from "react"; 12 12 import { toast } from "sonner"; 13 - import { Calendar as CalendarComponent } from "@/components/ui/calendar"; 14 13 import { LoadingButton } from "@/components/ui/loading-button"; 15 14 import { M3Button } from "@/components/ui/m3-button"; 16 - import { 17 - Popover, 18 - PopoverContent, 19 - PopoverTrigger, 20 - } from "@/components/ui/popover"; 15 + import { MaterialDatePicker } from "@/components/ui/material-date-picker"; 21 16 import { TimePicker } from "@/components/ui/time-picker"; 22 17 23 18 type DatePickerModalProps = { ··· 137 132 }); 138 133 }; 139 134 135 + const handleDateSelect = (date: Date) => { 136 + setCustomDate(format(date, "yyyy-MM-dd")); 137 + }; 138 + 140 139 if (!open) return null; 141 140 142 141 return ( 143 142 <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"> 144 - <div className="bg-[var(--md-sys-color-surface-container-high)] rounded-[1.75rem] p-6 max-w-md w-full"> 143 + <div className="bg-(--md-sys-color-surface-container-high) rounded-[1.75rem] p-6 max-w-sm w-full"> 145 144 <div className="flex justify-between items-center mb-6"> 146 - <h3 className="text-xl font-semibold text-[var(--md-sys-color-on-surface)]"> 147 - {modalTitle || "Watch Again"} 145 + <h3 className="text-xl font-semibold text-(--md-sys-color-on-surface)"> 146 + {modalTitle || "Select date"} 148 147 </h3> 149 148 <button 150 149 type="button" 151 150 onClick={onClose} 152 - className="p-2 hover:bg-[var(--md-sys-color-surface-container-high)] rounded-full transition-colors text-[var(--md-sys-color-on-surface-variant)]" 151 + className="p-2 hover:bg-(--md-sys-color-surface-container-highest) rounded-full transition-colors text-(--md-sys-color-on-surface-variant)" 153 152 > 154 153 <X className="w-5 h-5" /> 155 154 </button> 156 155 </div> 157 - <p className="text-[var(--md-sys-color-on-surface-variant)] mb-4"> 158 - When did you watch this? 159 - </p> 160 156 <div className="space-y-4"> 161 - <div> 162 - <label 163 - htmlFor="date-picker" 164 - className="block text-sm text-[var(--md-sys-color-on-surface-variant)] mb-2 cursor-pointer" 165 - > 166 - Date 167 - </label> 168 - <Popover> 169 - <PopoverTrigger asChild> 170 - <M3Button 171 - variant="outlined" 172 - className="w-full px-4 py-3 h-auto mt-2 bg-[var(--md-sys-color-surface-container-high)] rounded-xl border border-[var(--md-sys-color-outline)] text-[var(--md-sys-color-on-surface)] hover:bg-[var(--md-sys-color-surface-container-high)] justify-start text-left font-normal" 173 - > 174 - <Calendar className="mr-2 h-4 w-4 text-[var(--md-sys-color-on-surface-variant)]" /> 175 - {customDate ? ( 176 - format(new Date(customDate), "PPP") 177 - ) : ( 178 - <span className="text-[var(--md-sys-color-on-surface-variant)]"> 179 - Pick a date 180 - </span> 181 - )} 182 - </M3Button> 183 - </PopoverTrigger> 184 - <PopoverContent 185 - className="w-auto p-0 bg-[var(--md-sys-color-surface-container)] border-[var(--md-sys-color-outline)]" 186 - align="start" 187 - > 188 - <CalendarComponent 189 - mode="single" 190 - selected={customDate ? new Date(customDate) : undefined} 191 - onSelect={(date) => { 192 - if (date) { 193 - setCustomDate(format(date, "yyyy-MM-dd")); 194 - } 195 - }} 196 - autoFocus 197 - /> 198 - </PopoverContent> 199 - </Popover> 200 - </div> 157 + <MaterialDatePicker 158 + selected={customDate ? new Date(customDate) : new Date()} 159 + onSelect={handleDateSelect} 160 + /> 201 161 <div> 202 162 <TimePicker date={timeDate} setDate={setTimeDate} /> 203 163 </div> ··· 206 166 type="button" 207 167 variant="outlined" 208 168 onClick={onClose} 209 - className="flex-1 border-[var(--md-sys-color-outline)] text-[var(--md-sys-color-on-surface)] hover:bg-[var(--md-sys-color-surface-container-high)]" 169 + className="flex-1 border-(--md-sys-color-outline) text-(--md-sys-color-on-surface) hover:bg-(--md-sys-color-surface-container-highest)" 210 170 > 211 171 Cancel 212 172 </M3Button> ··· 218 178 markMutation.isPending || 219 179 markEpisodeMutation.isPending 220 180 } 221 - className="flex-1 bg-[var(--md-sys-color-primary)] hover:bg-[var(--md-sys-color-primary)]/90" 181 + className="flex-1 bg-(--md-sys-color-primary) hover:bg-(--md-sys-color-primary)/90" 222 182 isLoading={ 223 183 markMutation.isPending || markEpisodeMutation.isPending 224 184 }
+5 -5
apps/web/src/components/Header.tsx
··· 68 68 <nav className="hidden md:flex items-center gap-1"> 69 69 <Link 70 70 to="/" 71 - className="flex items-center gap-2 px-4 py-2 rounded-[var(--md-sys-shape-corner-large)] transition-colors md-label-large" 71 + className="flex items-center gap-2 px-4 py-2 rounded-(--md-sys-shape-corner-large) transition-colors md-label-large" 72 72 style={{ 73 73 color: "var(--md-sys-color-on-surface-variant)", 74 74 }} ··· 98 98 <Link 99 99 to="/search" 100 100 search={{ q: "", type: "all" }} 101 - className="flex items-center gap-2 px-4 py-2 rounded-[var(--md-sys-shape-corner-large)] transition-colors md-label-large" 101 + className="flex items-center gap-2 px-4 py-2 rounded-(--md-sys-shape-corner-large) transition-colors md-label-large" 102 102 style={{ 103 103 color: "var(--md-sys-color-on-surface-variant)", 104 104 }} ··· 133 133 <div className="flex items-center gap-3"> 134 134 <Link 135 135 to="/profile/shelf" 136 - className="flex items-center gap-3 rounded-[var(--md-sys-shape-corner-large)] px-2 py-1.5 transition-colors" 136 + className="flex items-center gap-3 rounded-(--md-sys-shape-corner-large) px-2 py-1.5 transition-colors" 137 137 style={{ 138 138 color: "var(--md-sys-color-on-surface-variant)", 139 139 }} ··· 235 235 <Link 236 236 to="/" 237 237 onClick={() => setIsOpen(false)} 238 - className="flex items-center gap-3 p-3 rounded-[var(--md-sys-shape-corner-large)] transition-colors mb-2 md-label-large" 238 + className="flex items-center gap-3 p-3 rounded-(--md-sys-shape-corner-large) transition-colors mb-2 md-label-large" 239 239 style={{ 240 240 color: "var(--md-sys-color-on-surface-variant)", 241 241 }} ··· 267 267 to="/search" 268 268 search={{ q: "", type: "all" }} 269 269 onClick={() => setIsOpen(false)} 270 - className="flex items-center gap-3 p-3 rounded-[var(--md-sys-shape-corner-large)] transition-colors mb-2 md-label-large" 270 + className="flex items-center gap-3 p-3 rounded-(--md-sys-shape-corner-large) transition-colors mb-2 md-label-large" 271 271 style={{ 272 272 color: "var(--md-sys-color-on-surface-variant)", 273 273 }}
+2 -2
apps/web/src/components/MovieHero.tsx
··· 43 43 <div className="container mx-auto max-w-6xl"> 44 44 <div className="flex items-end gap-4 md:gap-8"> 45 45 <div className="shrink-0"> 46 - <div className="w-28 md:w-48 lg:w-64 rounded-lg overflow-hidden bg-[var(--md-sys-color-surface-container)]" /> 46 + <div className="w-28 md:w-48 lg:w-64 rounded-lg overflow-hidden bg-(--md-sys-color-surface-container)" /> 47 47 </div> 48 48 <div className="flex-1 pb-2"> 49 - <div className="h-8 md:h-16 lg:w-96 bg-[var(--md-sys-color-surface-container)] rounded-lg animate-pulse" /> 49 + <div className="h-8 md:h-16 lg:w-96 bg-(--md-sys-color-surface-container) rounded-lg animate-pulse" /> 50 50 </div> 51 51 </div> 52 52 </div>
+1 -1
apps/web/src/components/ShelfEpisodeCard.tsx
··· 76 76 No poster 77 77 </div> 78 78 )} 79 - <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent p-3"> 79 + <div className="absolute inset-x-0 bottom-0 bg-linear-to-t from-black/80 to-transparent p-3"> 80 80 <div className="text-white text-sm font-medium"> 81 81 S{tracked.seasonNumber} E{tracked.episodeNumber} 82 82 </div>
+1 -1
apps/web/src/components/ui/action-button.tsx
··· 29 29 type="button" 30 30 onClick={onClick} 31 31 disabled={disabled} 32 - className={`w-full py-3 px-6 rounded-xl m3-label-large transition-all duration-200 flex items-center justify-center gap-2 border focus:outline-none focus:ring-2 focus:ring-[var(--md-sys-color-primary)]/50 ${className}`} 32 + className={`w-full py-3 px-6 rounded-xl m3-label-large transition-all duration-200 flex items-center justify-center gap-2 border focus:outline-none focus:ring-2 focus:ring-(--md-sys-color-primary)/50 ${className}`} 33 33 style={ 34 34 isActive 35 35 ? {
+42 -34
apps/web/src/components/ui/calendar.tsx
··· 42 42 children?: React.ReactNode; 43 43 }) { 44 44 if (orientation === "left") { 45 - return <ChevronLeftIcon className={cn("size-4", className)} {...props} />; 45 + return <ChevronLeftIcon className={cn("size-5", className)} {...props} />; 46 46 } 47 47 48 48 if (orientation === "right") { 49 - return <ChevronRightIcon className={cn("size-4", className)} {...props} />; 49 + return <ChevronRightIcon className={cn("size-5", className)} {...props} />; 50 50 } 51 51 52 52 return <ChevronDownIcon className={cn("size-4", className)} {...props} />; ··· 86 86 return ( 87 87 <DayPicker 88 88 showOutsideDays={showOutsideDays} 89 + weekStartsOn={1} 89 90 className={cn( 90 - "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", 91 + "bg-background group/calendar p-0 in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent font-normal", 91 92 String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, 92 93 String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, 93 94 className, ··· 96 97 formatters={{ 97 98 formatMonthDropdown: (date) => 98 99 date.toLocaleString("default", { month: "short" }), 100 + formatWeekdayShort: (date) => 101 + date.toLocaleString("default", { weekday: "narrow" }), 99 102 ...formatters, 100 103 }} 101 104 classNames={{ 102 105 root: cn("w-fit", defaultClassNames.root), 103 - months: cn( 104 - "flex gap-4 flex-col md:flex-row relative", 105 - defaultClassNames.months, 106 - ), 107 - month: cn("flex flex-col w-full gap-4", defaultClassNames.month), 106 + months: cn("flex flex-col gap-0", defaultClassNames.months), 107 + month: cn("flex flex-col w-full gap-0", defaultClassNames.month), 108 108 nav: cn( 109 - "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", 109 + "flex items-center justify-between w-full px-1 py-1", 110 110 defaultClassNames.nav, 111 111 ), 112 112 button_previous: cn( 113 113 buttonVariants({ variant: buttonVariant }), 114 - "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", 114 + "size-8 aria-disabled:opacity-50 p-0 rounded-full select-none hover:bg-muted", 115 115 defaultClassNames.button_previous, 116 116 ), 117 117 button_next: cn( 118 118 buttonVariants({ variant: buttonVariant }), 119 - "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", 119 + "size-8 aria-disabled:opacity-50 p-0 rounded-full select-none hover:bg-muted", 120 120 defaultClassNames.button_next, 121 121 ), 122 122 month_caption: cn( 123 - "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", 123 + "flex items-center justify-center h-10 w-full", 124 124 defaultClassNames.month_caption, 125 125 ), 126 126 dropdowns: cn( 127 - "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", 127 + "flex items-center text-sm font-medium justify-center h-10 gap-1", 128 128 defaultClassNames.dropdowns, 129 129 ), 130 - dropdown_root: cn( 131 - "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", 132 - defaultClassNames.dropdown_root, 133 - ), 130 + dropdown_root: cn("relative", defaultClassNames.dropdown_root), 134 131 dropdown: cn( 135 132 "absolute bg-popover inset-0 opacity-0", 136 133 defaultClassNames.dropdown, 137 134 ), 138 135 caption_label: cn( 139 - "select-none font-medium text-white", 136 + "select-none font-medium text-foreground text-base", 140 137 captionLayout === "label" 141 - ? "text-sm" 138 + ? "text-base font-medium" 142 139 : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", 143 140 defaultClassNames.caption_label, 144 141 ), 145 142 table: "w-full border-collapse", 146 - weekdays: cn("flex", defaultClassNames.weekdays), 143 + weekdays: cn("grid grid-cols-7 w-full", defaultClassNames.weekdays), 147 144 weekday: cn( 148 - "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", 145 + "text-muted-foreground font-normal text-xs select-none h-9 flex items-center justify-center", 149 146 defaultClassNames.weekday, 150 147 ), 151 - week: cn("flex w-full mt-2", defaultClassNames.week), 148 + week: cn("grid grid-cols-7 w-full", defaultClassNames.week), 152 149 week_number_header: cn( 153 - "select-none w-(--cell-size)", 150 + "select-none w-8", 154 151 defaultClassNames.week_number_header, 155 152 ), 156 153 week_number: cn( 157 - "text-[0.8rem] select-none text-muted-foreground", 154 + "text-xs select-none text-muted-foreground", 158 155 defaultClassNames.week_number, 159 156 ), 160 157 day: cn( 161 - "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", 158 + "relative w-full h-full p-0 text-center group/day aspect-square select-none", 162 159 props.showWeekNumber 163 - ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" 164 - : "[&:first-child[data-selected=true]_button]:rounded-l-md", 160 + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-full" 161 + : "[&:first-child[data-selected=true]_button]:rounded-l-full", 165 162 defaultClassNames.day, 166 163 ), 167 164 range_start: cn( 168 - "rounded-l-md bg-accent", 165 + "rounded-l-full rounded-r-none bg-primary text-primary-foreground", 169 166 defaultClassNames.range_start, 170 167 ), 171 - range_middle: cn("rounded-none", defaultClassNames.range_middle), 172 - range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), 168 + range_middle: cn( 169 + "rounded-none bg-primary/10 text-foreground", 170 + defaultClassNames.range_middle, 171 + ), 172 + range_end: cn( 173 + "rounded-r-full rounded-l-none bg-primary text-primary-foreground", 174 + defaultClassNames.range_end, 175 + ), 173 176 today: cn( 174 - "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", 177 + "border-2 border-primary rounded-full font-semibold text-foreground", 175 178 defaultClassNames.today, 176 179 ), 180 + selected: cn( 181 + "bg-primary text-primary-foreground rounded-full font-semibold", 182 + defaultClassNames.selected, 183 + ), 177 184 outside: cn( 178 - "text-muted-foreground aria-selected:text-muted-foreground", 185 + "text-muted-foreground/50 aria-selected:text-muted-foreground", 179 186 defaultClassNames.outside, 180 187 ), 181 188 disabled: cn( 182 - "text-muted-foreground opacity-50", 189 + "text-muted-foreground/30 opacity-50", 183 190 defaultClassNames.disabled, 184 191 ), 185 192 hidden: cn("invisible", defaultClassNames.hidden), ··· 225 232 data-range-start={modifiers.range_start} 226 233 data-range-end={modifiers.range_end} 227 234 data-range-middle={modifiers.range_middle} 235 + data-today={modifiers.today} 228 236 className={cn( 229 - "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70", 237 + "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-primary/10 data-[range-middle=true]:text-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[today=true]:border-2 data-[today=true]:border-primary data-[today=true]:rounded-full group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square w-full h-full min-w-0 min-h-0 p-0 leading-none font-normal justify-center items-center rounded-full hover:bg-muted group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-2 group-data-[focused=true]/day:ring-ring", 230 238 defaultClassNames.day, 231 239 className, 232 240 )}
+4 -4
apps/web/src/components/ui/dialog.tsx
··· 52 52 <DialogPrimitive.Content 53 53 data-slot="dialog-content" 54 54 className={cn( 55 - "bg-[var(--md-sys-color-surface-container-high)] text-[var(--md-sys-color-on-surface)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-[1.75rem] border border-[var(--md-sys-color-outline)] p-6 shadow-lg duration-200 outline-none sm:max-w-lg", 55 + "bg-(--md-sys-color-surface-container-high) text-(--md-sys-color-on-surface) data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-[1.75rem] border border-(--md-sys-color-outline) p-6 shadow-lg duration-200 outline-none sm:max-w-lg", 56 56 className, 57 57 )} 58 58 {...props} ··· 61 61 {showCloseButton && ( 62 62 <DialogPrimitive.Close 63 63 data-slot="dialog-close" 64 - className="text-[var(--md-sys-color-on-surface-variant)] hover:text-[var(--md-sys-color-on-surface)] focus:ring-[var(--md-sys-color-primary)]/50 absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" 64 + className="text-(--md-sys-color-on-surface-variant) hover:text-(--md-sys-color-on-surface) focus:ring-(--md-sys-color-primary)/50 absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" 65 65 > 66 66 <XIcon /> 67 67 <span className="sr-only">Close</span> ··· 117 117 <DialogPrimitive.Title 118 118 data-slot="dialog-title" 119 119 className={cn( 120 - "text-[var(--md-sys-color-on-surface)] text-lg leading-none font-semibold", 120 + "text-(--md-sys-color-on-surface) text-lg leading-none font-semibold", 121 121 className, 122 122 )} 123 123 {...props} ··· 133 133 <DialogPrimitive.Description 134 134 data-slot="dialog-description" 135 135 className={cn( 136 - "text-[var(--md-sys-color-on-surface-variant)] text-sm", 136 + "text-(--md-sys-color-on-surface-variant) text-sm", 137 137 className, 138 138 )} 139 139 {...props}
+10 -10
apps/web/src/components/ui/m3-text-field.tsx
··· 156 156 > 157 157 {/* Leading Icon */} 158 158 {leadingIcon && ( 159 - <div className="absolute left-4 top-1/2 -translate-y-1/2 text-[var(--md-sys-color-on-surface-variant)]"> 159 + <div className="absolute left-4 top-1/2 -translate-y-1/2 text-(--md-sys-color-on-surface-variant)"> 160 160 {leadingIcon} 161 161 </div> 162 162 )} ··· 173 173 "md-label-small", 174 174 variant === "filled" && "top-2", 175 175 variant === "outlined" && 176 - "top-0 -translate-y-1/2 bg-[var(--md-sys-color-surface)] px-1", 176 + "top-0 -translate-y-1/2 bg-(--md-sys-color-surface) px-1", 177 177 ], 178 178 !isLabelFloating && 179 - "md-body-large text-[var(--md-sys-color-on-surface-variant)]", 179 + "md-body-large text-(--md-sys-color-on-surface-variant)", 180 180 isLabelFloating && 181 181 (error 182 - ? "text-[var(--md-sys-color-error)]" 183 - : "text-[var(--md-sys-color-primary)]"), 182 + ? "text-(--md-sys-color-error)" 183 + : "text-(--md-sys-color-primary)"), 184 184 leadingIcon && !isLabelFloating && "left-12", 185 185 )} 186 186 > ··· 228 228 onClear(); 229 229 } 230 230 }} 231 - className="text-[var(--md-sys-color-on-surface-variant)] hover:text-[var(--md-sys-color-on-surface)]" 231 + className="text-(--md-sys-color-on-surface-variant) hover:text-(--md-sys-color-on-surface)" 232 232 > 233 233 <X className="w-5 h-5" /> 234 234 </button> ··· 239 239 <button 240 240 type="button" 241 241 onClick={() => setIsPasswordVisible(!isPasswordVisible)} 242 - className="text-[var(--md-sys-color-on-surface-variant)] hover:text-[var(--md-sys-color-on-surface)]" 242 + className="text-(--md-sys-color-on-surface-variant) hover:text-(--md-sys-color-on-surface)" 243 243 > 244 244 {isPasswordVisible ? ( 245 245 <EyeOff className="w-5 h-5" /> ··· 251 251 252 252 {/* Custom Trailing Icon */} 253 253 {trailingIcon && type !== "password" && !showClearButton && ( 254 - <span className="text-[var(--md-sys-color-on-surface-variant)]"> 254 + <span className="text-(--md-sys-color-on-surface-variant)"> 255 255 {trailingIcon} 256 256 </span> 257 257 )} ··· 264 264 className={cn( 265 265 "mt-1 ml-4 md-body-small", 266 266 errorMessage 267 - ? "text-[var(--md-sys-color-error)]" 268 - : "text-[var(--md-sys-color-on-surface-variant)]", 267 + ? "text-(--md-sys-color-error)" 268 + : "text-(--md-sys-color-on-surface-variant)", 269 269 )} 270 270 > 271 271 {errorMessage || supportingText}
+266
apps/web/src/components/ui/material-date-picker.tsx
··· 1 + import { 2 + addDays, 3 + addMonths, 4 + endOfMonth, 5 + endOfWeek, 6 + format, 7 + isSameDay, 8 + isSameMonth, 9 + isToday, 10 + startOfMonth, 11 + startOfWeek, 12 + } from "date-fns"; 13 + import { ChevronLeft, ChevronRight } from "lucide-react"; 14 + import * as React from "react"; 15 + import { cn } from "@/lib/utils"; 16 + 17 + interface MaterialDatePickerProps { 18 + selected?: Date; 19 + onSelect?: (date: Date) => void; 20 + className?: string; 21 + } 22 + 23 + export function MaterialDatePicker({ 24 + selected, 25 + onSelect, 26 + className, 27 + }: MaterialDatePickerProps) { 28 + const [currentMonth, setCurrentMonth] = React.useState( 29 + selected || new Date(), 30 + ); 31 + const [selectingYear, setSelectingYear] = React.useState(false); 32 + 33 + // Update current month when selected changes from outside 34 + React.useEffect(() => { 35 + if (selected) { 36 + setCurrentMonth(selected); 37 + } 38 + }, [selected]); 39 + 40 + const selectedDate = selected || new Date(); 41 + 42 + const handlePrevMonth = () => { 43 + setCurrentMonth(addMonths(currentMonth, -1)); 44 + }; 45 + 46 + const handleNextMonth = () => { 47 + setCurrentMonth(addMonths(currentMonth, 1)); 48 + }; 49 + 50 + const handleDateSelect = (date: Date) => { 51 + onSelect?.(date); 52 + }; 53 + 54 + // Generate calendar days 55 + const monthStart = startOfMonth(currentMonth); 56 + const monthEnd = endOfMonth(monthStart); 57 + const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 }); 58 + const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 }); 59 + 60 + const days: Date[] = []; 61 + let day = calendarStart; 62 + while (day <= calendarEnd) { 63 + days.push(day); 64 + day = addDays(day, 1); 65 + } 66 + 67 + const weekDays = [ 68 + { label: "S", id: "sun" }, 69 + { label: "M", id: "mon" }, 70 + { label: "T", id: "tue" }, 71 + { label: "W", id: "wed" }, 72 + { label: "T", id: "thu" }, 73 + { label: "F", id: "fri" }, 74 + { label: "S", id: "sat" }, 75 + ]; 76 + 77 + // Generate years for year picker (100 years back, 50 years forward) 78 + const currentYear = new Date().getFullYear(); 79 + const years = Array.from({ length: 151 }, (_, i) => currentYear - 100 + i); 80 + 81 + // Generate months 82 + const months = [ 83 + "January", 84 + "February", 85 + "March", 86 + "April", 87 + "May", 88 + "June", 89 + "July", 90 + "August", 91 + "September", 92 + "October", 93 + "November", 94 + "December", 95 + ]; 96 + 97 + const handleYearSelect = (year: number) => { 98 + const newDate = new Date(currentMonth); 99 + newDate.setFullYear(year); 100 + setCurrentMonth(newDate); 101 + setSelectingYear(false); 102 + }; 103 + 104 + const handleMonthSelect = (monthIndex: number) => { 105 + const newDate = new Date(currentMonth); 106 + newDate.setMonth(monthIndex); 107 + setCurrentMonth(newDate); 108 + }; 109 + 110 + return ( 111 + <div 112 + className={cn( 113 + "w-[328px] bg-(--md-sys-color-surface-container-high) rounded-[28px] p-0 overflow-hidden", 114 + className, 115 + )} 116 + > 117 + {/* Header with selected date */} 118 + <div className="px-6 pt-6 pb-4"> 119 + <div className="text-(--md-sys-color-on-surface-variant) text-sm mb-2"> 120 + Select date 121 + </div> 122 + <div className="text-(--md-sys-color-on-surface) text-[32px] leading-[40px] font-normal tracking-[-0.25px]"> 123 + {format(selectedDate, "EEE, MMM d")} 124 + </div> 125 + </div> 126 + 127 + {/* Divider */} 128 + <div className="h-px bg-(--md-sys-color-outline-variant) mx-4" /> 129 + 130 + {/* Month/Year Navigation */} 131 + <div className="flex items-center justify-between px-4 py-3"> 132 + <button 133 + type="button" 134 + onClick={() => setSelectingYear(!selectingYear)} 135 + className="flex items-center gap-1 text-(--md-sys-color-on-surface-variant) hover:text-(--md-sys-color-on-surface) transition-colors" 136 + > 137 + <span className="text-sm font-medium"> 138 + {format(currentMonth, "MMMM yyyy")} 139 + </span> 140 + <svg 141 + className="w-4 h-4" 142 + viewBox="0 0 24 24" 143 + fill="none" 144 + stroke="currentColor" 145 + strokeWidth="2" 146 + role="img" 147 + > 148 + <title>Toggle month/year selector</title> 149 + <path d="M6 9l6 6 6-6" /> 150 + </svg> 151 + </button> 152 + <div className="flex items-center gap-1"> 153 + <button 154 + type="button" 155 + onClick={handlePrevMonth} 156 + className="w-10 h-10 flex items-center justify-center rounded-full text-(--md-sys-color-on-surface-variant) hover:bg-(--md-sys-color-on-surface-variant)/8 transition-colors" 157 + > 158 + <ChevronLeft className="w-5 h-5" /> 159 + </button> 160 + <button 161 + type="button" 162 + onClick={handleNextMonth} 163 + className="w-10 h-10 flex items-center justify-center rounded-full text-(--md-sys-color-on-surface-variant) hover:bg-(--md-sys-color-on-surface-variant)/8 transition-colors" 164 + > 165 + <ChevronRight className="w-5 h-5" /> 166 + </button> 167 + </div> 168 + </div> 169 + 170 + {/* Year/Month Picker Overlay */} 171 + {selectingYear && ( 172 + <div className="px-4 pb-4"> 173 + <div className="bg-(--md-sys-color-surface-container) rounded-[16px] p-4 max-h-[280px] overflow-y-auto"> 174 + <div className="grid grid-cols-3 gap-2"> 175 + {months.map((month, index) => ( 176 + <button 177 + key={month} 178 + type="button" 179 + onClick={() => handleMonthSelect(index)} 180 + className={cn( 181 + "px-3 py-2 rounded-full text-sm font-medium transition-colors", 182 + currentMonth.getMonth() === index 183 + ? "bg-(--md-sys-color-primary) text-(--md-sys-color-on-primary)" 184 + : "text-(--md-sys-color-on-surface-variant) hover:bg-(--md-sys-color-on-surface-variant)/8", 185 + )} 186 + > 187 + {month.slice(0, 3)} 188 + </button> 189 + ))} 190 + </div> 191 + <div className="h-px bg-(--md-sys-color-outline-variant) my-3" /> 192 + <div className="grid grid-cols-3 gap-2"> 193 + {years.map((year) => ( 194 + <button 195 + key={year} 196 + type="button" 197 + onClick={() => handleYearSelect(year)} 198 + className={cn( 199 + "px-3 py-2 rounded-full text-sm font-medium transition-colors", 200 + currentMonth.getFullYear() === year 201 + ? "bg-(--md-sys-color-primary) text-(--md-sys-color-on-primary)" 202 + : "text-(--md-sys-color-on-surface-variant) hover:bg-(--md-sys-color-on-surface-variant)/8", 203 + )} 204 + > 205 + {year} 206 + </button> 207 + ))} 208 + </div> 209 + </div> 210 + </div> 211 + )} 212 + 213 + {/* Calendar Grid */} 214 + {!selectingYear && ( 215 + <div className="px-2 pb-6"> 216 + {/* Weekday Headers */} 217 + <div className="grid grid-cols-7 mb-2"> 218 + {weekDays.map((weekDay) => ( 219 + <div 220 + key={weekDay.id} 221 + className="h-10 flex items-center justify-center text-(--md-sys-color-on-surface-variant) text-sm font-medium" 222 + > 223 + {weekDay.label} 224 + </div> 225 + ))} 226 + </div> 227 + 228 + {/* Days Grid */} 229 + <div className="grid grid-cols-7 gap-0"> 230 + {days.map((date) => { 231 + const isSelected = isSameDay(date, selectedDate); 232 + const isCurrentMonth = isSameMonth(date, currentMonth); 233 + const isTodayDate = isToday(date); 234 + const dateKey = format(date, "yyyy-MM-dd"); 235 + 236 + return ( 237 + <button 238 + key={dateKey} 239 + type="button" 240 + onClick={() => handleDateSelect(date)} 241 + className={cn( 242 + "h-10 w-10 mx-auto flex items-center justify-center text-sm font-normal rounded-full transition-all", 243 + !isCurrentMonth && 244 + "text-(--md-sys-color-on-surface-variant)/40", 245 + isCurrentMonth && 246 + !isSelected && 247 + "text-(--md-sys-color-on-surface)", 248 + isSelected && 249 + "bg-(--md-sys-color-primary) text-(--md-sys-color-on-primary)", 250 + !isSelected && 251 + isTodayDate && 252 + "border border-(--md-sys-color-primary) text-(--md-sys-color-primary)", 253 + !isSelected && 254 + "hover:bg-(--md-sys-color-on-surface-variant)/8", 255 + )} 256 + > 257 + {format(date, "d")} 258 + </button> 259 + ); 260 + })} 261 + </div> 262 + </div> 263 + )} 264 + </div> 265 + ); 266 + }
+1 -1
apps/web/src/components/ui/tooltip.tsx
··· 48 48 {...props} 49 49 > 50 50 {children} 51 - <TooltipPrimitive.Arrow className="bg-gray-100 fill-gray-100 z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> 51 + <TooltipPrimitive.Arrow className="bg-gray-100 fill-gray-100 z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]" /> 52 52 </TooltipPrimitive.Content> 53 53 </TooltipPrimitive.Portal> 54 54 );
+137 -51
apps/web/src/routes/movies.$movieId.$title.tsx
··· 296 296 {user ? ( 297 297 !isWatched ? ( 298 298 <> 299 - <AddToShelfButton 300 - onClick={handleMarkWatched} 301 - isPending={isPending} 302 - label="Add to Shelf" 303 - icon={<Calendar className="w-5 h-5" />} 304 - colors={colors} 305 - size="compact" 306 - /> 307 - <ActionButton 308 - icon={<Calendar className="w-4 h-4" />} 309 - label="Watch on different date" 310 - onClick={() => setShowDateModal(true)} 311 - /> 299 + <div className="flex gap-2"> 300 + <AddToShelfButton 301 + onClick={handleMarkWatched} 302 + isPending={isPending} 303 + label="Add to Shelf" 304 + icon={<Calendar className="w-5 h-5" />} 305 + colors={colors} 306 + size="compact" 307 + className="flex-1" 308 + /> 309 + <button 310 + type="button" 311 + onClick={() => setShowDateModal(true)} 312 + title="Watch movie" 313 + className="p-3 rounded-xl border transition-all duration-200 flex items-center justify-center group" 314 + style={{ 315 + backgroundColor: "transparent", 316 + borderColor: "var(--md-sys-color-outline)", 317 + }} 318 + onMouseEnter={(e) => { 319 + e.currentTarget.style.backgroundColor = 320 + "var(--md-sys-color-surface-container)"; 321 + e.currentTarget.style.borderColor = 322 + "var(--md-sys-color-primary)"; 323 + }} 324 + onMouseLeave={(e) => { 325 + e.currentTarget.style.backgroundColor = 326 + "transparent"; 327 + e.currentTarget.style.borderColor = 328 + "var(--md-sys-color-outline)"; 329 + }} 330 + > 331 + <Calendar className="w-5 h-5 text-(--md-sys-color-on-surface-variant) group-hover:text-(--md-sys-color-primary) transition-colors" /> 332 + </button> 333 + </div> 312 334 <ActionButton 313 335 icon={ 314 336 isInAnyList ? ( ··· 329 351 </> 330 352 ) : ( 331 353 <> 332 - <AddToShelfButton 333 - onClick={handleMarkWatched} 334 - isPending={isPending} 335 - label="Watch Now" 336 - icon={<RotateCcw className="w-4 h-4" />} 337 - colors={colors} 338 - size="compact" 339 - /> 340 - <ActionButton 341 - icon={<Calendar className="w-4 h-4" />} 342 - label="Watch on different date" 343 - onClick={() => setShowDateModal(true)} 344 - /> 354 + <div className="flex gap-2"> 355 + <AddToShelfButton 356 + onClick={handleMarkWatched} 357 + isPending={isPending} 358 + label="Watch Now" 359 + icon={<RotateCcw className="w-4 h-4" />} 360 + colors={colors} 361 + size="compact" 362 + className="flex-1" 363 + /> 364 + <button 365 + type="button" 366 + onClick={() => setShowDateModal(true)} 367 + title="Watch movie" 368 + className="p-3 rounded-xl border transition-all duration-200 flex items-center justify-center group" 369 + style={{ 370 + backgroundColor: "transparent", 371 + borderColor: "var(--md-sys-color-outline)", 372 + }} 373 + onMouseEnter={(e) => { 374 + e.currentTarget.style.backgroundColor = 375 + "var(--md-sys-color-surface-container)"; 376 + e.currentTarget.style.borderColor = 377 + "var(--md-sys-color-primary)"; 378 + }} 379 + onMouseLeave={(e) => { 380 + e.currentTarget.style.backgroundColor = 381 + "transparent"; 382 + e.currentTarget.style.borderColor = 383 + "var(--md-sys-color-outline)"; 384 + }} 385 + > 386 + <Calendar className="w-5 h-5 text-(--md-sys-color-on-surface-variant) group-hover:text-(--md-sys-color-primary) transition-colors" /> 387 + </button> 388 + </div> 345 389 <ActionButton 346 390 icon={ 347 391 isInAnyList ? ( ··· 388 432 {user ? ( 389 433 !isWatched ? ( 390 434 <div className="space-y-3"> 391 - <AddToShelfButton 392 - onClick={handleMarkWatched} 393 - isPending={isPending} 394 - label="Add to Shelf" 395 - icon={<Calendar className="w-5 h-5" />} 396 - colors={colors} 397 - /> 398 - <ActionButton 399 - icon={<Calendar className="w-4 h-4" />} 400 - label="Watch on different date" 401 - onClick={() => setShowDateModal(true)} 402 - /> 435 + <div className="flex gap-2"> 436 + <AddToShelfButton 437 + onClick={handleMarkWatched} 438 + isPending={isPending} 439 + label="Add to Shelf" 440 + icon={<Calendar className="w-5 h-5" />} 441 + colors={colors} 442 + className="flex-1" 443 + /> 444 + <button 445 + type="button" 446 + onClick={() => setShowDateModal(true)} 447 + title="Watch movie" 448 + className="p-3 rounded-xl border transition-all duration-200 flex items-center justify-center group" 449 + style={{ 450 + backgroundColor: "transparent", 451 + borderColor: "var(--md-sys-color-outline)", 452 + }} 453 + onMouseEnter={(e) => { 454 + e.currentTarget.style.backgroundColor = 455 + "var(--md-sys-color-surface-container)"; 456 + e.currentTarget.style.borderColor = 457 + "var(--md-sys-color-primary)"; 458 + }} 459 + onMouseLeave={(e) => { 460 + e.currentTarget.style.backgroundColor = "transparent"; 461 + e.currentTarget.style.borderColor = 462 + "var(--md-sys-color-outline)"; 463 + }} 464 + > 465 + <Calendar className="w-5 h-5 text-(--md-sys-color-on-surface-variant) group-hover:text-(--md-sys-color-primary) transition-colors" /> 466 + </button> 467 + </div> 403 468 <ActionButton 404 469 icon={ 405 470 isInAnyList ? ( ··· 511 576 </button> 512 577 )} 513 578 </div> 514 - <AddToShelfButton 515 - onClick={handleMarkWatched} 516 - isPending={isPending} 517 - label="Watch Again" 518 - icon={<RotateCcw className="w-4 h-4" />} 519 - colors={colors} 520 - size="compact" 521 - /> 522 - <ActionButton 523 - icon={<Calendar className="w-4 h-4" />} 524 - label="Watch on different date" 525 - onClick={() => setShowDateModal(true)} 526 - /> 579 + <div className="flex gap-2"> 580 + <AddToShelfButton 581 + onClick={handleMarkWatched} 582 + isPending={isPending} 583 + label="Watch Again" 584 + icon={<RotateCcw className="w-4 h-4" />} 585 + colors={colors} 586 + size="compact" 587 + className="flex-1" 588 + /> 589 + <button 590 + type="button" 591 + onClick={() => setShowDateModal(true)} 592 + title="Watch movie" 593 + className="p-3 rounded-xl border transition-all duration-200 flex items-center justify-center group" 594 + style={{ 595 + backgroundColor: "transparent", 596 + borderColor: "var(--md-sys-color-outline)", 597 + }} 598 + onMouseEnter={(e) => { 599 + e.currentTarget.style.backgroundColor = 600 + "var(--md-sys-color-surface-container)"; 601 + e.currentTarget.style.borderColor = 602 + "var(--md-sys-color-primary)"; 603 + }} 604 + onMouseLeave={(e) => { 605 + e.currentTarget.style.backgroundColor = "transparent"; 606 + e.currentTarget.style.borderColor = 607 + "var(--md-sys-color-outline)"; 608 + }} 609 + > 610 + <Calendar className="w-5 h-5 text-(--md-sys-color-on-surface-variant) group-hover:text-(--md-sys-color-primary) transition-colors" /> 611 + </button> 612 + </div> 527 613 <ActionButton 528 614 icon={ 529 615 isInAnyList ? (
+67 -25
apps/web/src/routes/shows.$showId.$title.seasons.$seasonNumber.episodes.$episodeNumber.tsx
··· 390 390 {user ? ( 391 391 !isWatchedEpisode ? ( 392 392 <div className="space-y-3"> 393 - <AddToShelfButton 394 - onClick={handleMarkWatched} 395 - isPending={isPending} 396 - label="Add to Shelf" 397 - icon={<Calendar className="w-5 h-5" />} 398 - colors={colors} 399 - /> 400 - <ActionButton 401 - icon={<Calendar className="w-4 h-4" />} 402 - label="Watch on different date" 403 - onClick={() => setShowDateModal(true)} 404 - /> 393 + <div className="flex gap-2"> 394 + <AddToShelfButton 395 + onClick={handleMarkWatched} 396 + isPending={isPending} 397 + label="Add to Shelf" 398 + icon={<Calendar className="w-5 h-5" />} 399 + colors={colors} 400 + className="flex-1" 401 + /> 402 + <button 403 + type="button" 404 + onClick={() => setShowDateModal(true)} 405 + title="Watch episode" 406 + className="p-3 rounded-xl border transition-all duration-200 flex items-center justify-center group" 407 + style={{ 408 + backgroundColor: "transparent", 409 + borderColor: "var(--md-sys-color-outline)", 410 + }} 411 + onMouseEnter={(e) => { 412 + e.currentTarget.style.backgroundColor = 413 + "var(--md-sys-color-surface-container)"; 414 + e.currentTarget.style.borderColor = 415 + "var(--md-sys-color-primary)"; 416 + }} 417 + onMouseLeave={(e) => { 418 + e.currentTarget.style.backgroundColor = "transparent"; 419 + e.currentTarget.style.borderColor = 420 + "var(--md-sys-color-outline)"; 421 + }} 422 + > 423 + <Calendar className="w-5 h-5 text-(--md-sys-color-on-surface-variant) group-hover:text-(--md-sys-color-primary) transition-colors" /> 424 + </button> 425 + </div> 405 426 <ActionButton 406 427 icon={ 407 428 isInAnyList ? ( ··· 521 542 </button> 522 543 )} 523 544 </div> 524 - <AddToShelfButton 525 - onClick={handleMarkWatched} 526 - isPending={isPending} 527 - label="Watch Again" 528 - icon={<RotateCcw className="w-4 h-4" />} 529 - colors={colors} 530 - size="compact" 531 - /> 532 - <ActionButton 533 - icon={<Calendar className="w-4 h-4" />} 534 - label="Watch on different date" 535 - onClick={() => setShowDateModal(true)} 536 - /> 545 + <div className="flex gap-2"> 546 + <AddToShelfButton 547 + onClick={handleMarkWatched} 548 + isPending={isPending} 549 + label="Watch Again" 550 + icon={<RotateCcw className="w-4 h-4" />} 551 + colors={colors} 552 + size="compact" 553 + className="flex-1" 554 + /> 555 + <button 556 + type="button" 557 + onClick={() => setShowDateModal(true)} 558 + title="Watch episode" 559 + className="p-3 rounded-xl border transition-all duration-200 flex items-center justify-center group" 560 + style={{ 561 + backgroundColor: "transparent", 562 + borderColor: "var(--md-sys-color-outline)", 563 + }} 564 + onMouseEnter={(e) => { 565 + e.currentTarget.style.backgroundColor = 566 + "var(--md-sys-color-surface-container)"; 567 + e.currentTarget.style.borderColor = 568 + "var(--md-sys-color-primary)"; 569 + }} 570 + onMouseLeave={(e) => { 571 + e.currentTarget.style.backgroundColor = "transparent"; 572 + e.currentTarget.style.borderColor = 573 + "var(--md-sys-color-outline)"; 574 + }} 575 + > 576 + <Calendar className="w-5 h-5 text-(--md-sys-color-on-surface-variant) group-hover:text-(--md-sys-color-primary) transition-colors" /> 577 + </button> 578 + </div> 537 579 <ActionButton 538 580 icon={ 539 581 isInAnyList ? (
+2 -1
packages/api/src/client.ts
··· 70 70 } 71 71 72 72 // Simple URL helper for login (not an API call) 73 - export function getLoginUrl(handle?: string, timezone?: string): string { 73 + export function getLoginUrl(handle?: string, timezone?: string, platform?: string): string { 74 74 const params = new URLSearchParams(); 75 75 if (handle) params.set("handle", handle); 76 76 if (timezone) params.set("timezone", timezone); 77 + if (platform) params.set("platform", platform); 77 78 const queryString = params.toString(); 78 79 return `${baseUrl}/auth/login${queryString ? `?${queryString}` : ""}`; 79 80 }
+3
pnpm-lock.yaml
··· 107 107 react-native-gesture-handler: 108 108 specifier: ~2.28.0 109 109 version: 2.28.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 110 + react-native-paper: 111 + specifier: ^5.15.0 112 + version: 5.15.0(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 110 113 react-native-paper-dates: 111 114 specifier: ^0.23.3 112 115 version: 0.23.3(react-native-paper@5.15.0(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)