kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

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

feat(web): show exact comment timestamp on relative time hover

Tin ba8cbfed 9ac30bbb

+133 -114
+121 -114
apps/web/src/components/activity/comment-card.tsx
··· 18 18 TooltipTrigger, 19 19 } from "@/components/ui/tooltip"; 20 20 import useUpdateComment from "@/hooks/mutations/comment/use-update-comment"; 21 - import { formatRelativeTime } from "@/lib/format"; 21 + import { formatDateTime, formatRelativeTime } from "@/lib/format"; 22 22 import { toast } from "@/lib/toast"; 23 23 24 24 type CommentCardProps = { ··· 103 103 ]); 104 104 105 105 return ( 106 - <div className="group relative w-full rounded-xl border border-border/80 bg-card/60"> 107 - <div className="flex items-center gap-2 px-3 pt-2.5"> 108 - <HoverCard> 109 - <HoverCardTrigger> 110 - <div className="flex cursor-pointer items-center gap-2"> 111 - <Avatar className="h-6 w-6"> 112 - <AvatarImage src={user?.image ?? ""} alt={user?.name || ""} /> 113 - <AvatarFallback className="bg-muted text-xs font-medium"> 114 - {user?.name?.charAt(0).toUpperCase()} 115 - </AvatarFallback> 116 - </Avatar> 117 - <span className="text-sm font-medium text-foreground/92 hover:text-foreground transition-colors"> 118 - {user?.name} 119 - </span> 120 - </div> 121 - </HoverCardTrigger> 122 - <HoverCardContent className="w-64 p-3"> 123 - <div className="flex items-center gap-3"> 124 - <Avatar className="h-10 w-10"> 125 - <AvatarImage src={user?.image ?? ""} alt={user?.name || ""} /> 126 - <AvatarFallback className="bg-muted text-xs font-medium"> 127 - {user?.name?.charAt(0).toUpperCase()} 128 - </AvatarFallback> 129 - </Avatar> 130 - <div className="min-w-0 flex-1"> 131 - <p className="text-sm font-medium text-foreground leading-none"> 106 + <TooltipProvider> 107 + <div className="group relative w-full rounded-xl border border-border/80 bg-card/60"> 108 + <div className="flex items-center gap-2 px-3 pt-2.5"> 109 + <HoverCard> 110 + <HoverCardTrigger> 111 + <div className="flex cursor-pointer items-center gap-2"> 112 + <Avatar className="h-6 w-6"> 113 + <AvatarImage src={user?.image ?? ""} alt={user?.name || ""} /> 114 + <AvatarFallback className="bg-muted text-xs font-medium"> 115 + {user?.name?.charAt(0).toUpperCase()} 116 + </AvatarFallback> 117 + </Avatar> 118 + <span className="text-sm font-medium text-foreground/92 hover:text-foreground transition-colors"> 132 119 {user?.name} 133 - </p> 134 - {user?.email && ( 135 - <p className="mt-1 text-xs text-muted-foreground"> 136 - {user.email} 120 + </span> 121 + </div> 122 + </HoverCardTrigger> 123 + <HoverCardContent className="w-64 p-3"> 124 + <div className="flex items-center gap-3"> 125 + <Avatar className="h-10 w-10"> 126 + <AvatarImage src={user?.image ?? ""} alt={user?.name || ""} /> 127 + <AvatarFallback className="bg-muted text-xs font-medium"> 128 + {user?.name?.charAt(0).toUpperCase()} 129 + </AvatarFallback> 130 + </Avatar> 131 + <div className="min-w-0 flex-1"> 132 + <p className="text-sm font-medium text-foreground leading-none"> 133 + {user?.name} 137 134 </p> 138 - )} 139 - {isFromGitHub && ( 140 - <div className="mt-1.5 flex items-center gap-1"> 141 - <Github className="size-3 text-muted-foreground" /> 142 - <span className="text-xs text-muted-foreground"> 143 - {t("activity:comment.github")} 144 - </span> 145 - </div> 146 - )} 135 + {user?.email && ( 136 + <p className="mt-1 text-xs text-muted-foreground"> 137 + {user.email} 138 + </p> 139 + )} 140 + {isFromGitHub && ( 141 + <div className="mt-1.5 flex items-center gap-1"> 142 + <Github className="size-3 text-muted-foreground" /> 143 + <span className="text-xs text-muted-foreground"> 144 + {t("activity:comment.github")} 145 + </span> 146 + </div> 147 + )} 148 + </div> 147 149 </div> 148 - </div> 149 - {githubProfileUrl && ( 150 + {githubProfileUrl && ( 151 + <a 152 + href={githubProfileUrl} 153 + target="_blank" 154 + rel="noopener noreferrer" 155 + className="mt-3 flex items-center gap-1.5 border-t border-border pt-3 text-xs text-muted-foreground transition-colors hover:text-foreground" 156 + > 157 + <ExternalLink className="size-3" /> 158 + {t("activity:comment.viewGithubProfile")} 159 + </a> 160 + )} 161 + </HoverCardContent> 162 + </HoverCard> 163 + 164 + <Tooltip> 165 + <TooltipTrigger asChild> 166 + <span className="cursor-default text-xs text-muted-foreground/62"> 167 + {formatRelativeTime(createdAt)} 168 + </span> 169 + </TooltipTrigger> 170 + <TooltipContent> 171 + <p className="text-xs">{formatDateTime(createdAt)}</p> 172 + </TooltipContent> 173 + </Tooltip> 174 + 175 + {commentUrl && ( 176 + <> 177 + <span className="text-xs text-muted-foreground/40">·</span> 150 178 <a 151 - href={githubProfileUrl} 179 + href={commentUrl} 152 180 target="_blank" 153 181 rel="noopener noreferrer" 154 - className="mt-3 flex items-center gap-1.5 border-t border-border pt-3 text-xs text-muted-foreground transition-colors hover:text-foreground" 182 + className="flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground" 155 183 > 156 - <ExternalLink className="size-3" /> 157 - {t("activity:comment.viewGithubProfile")} 184 + <Github className="size-3" /> 185 + {t("activity:comment.commentedOnGithub")} 158 186 </a> 159 - )} 160 - </HoverCardContent> 161 - </HoverCard> 162 - 163 - <span className="text-xs text-muted-foreground/62"> 164 - {formatRelativeTime(createdAt)} 165 - </span> 166 - 167 - {commentUrl && ( 168 - <> 169 - <span className="text-xs text-muted-foreground/40">·</span> 170 - <a 171 - href={commentUrl} 172 - target="_blank" 173 - rel="noopener noreferrer" 174 - className="flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground" 175 - > 176 - <Github className="size-3" /> 177 - {t("activity:comment.commentedOnGithub")} 178 - </a> 179 - </> 180 - )} 181 - </div> 187 + </> 188 + )} 189 + </div> 182 190 183 - {canEdit && !isEditing && ( 184 - <TooltipProvider> 191 + {canEdit && !isEditing && ( 185 192 <Tooltip> 186 193 <TooltipTrigger asChild> 187 194 <Button ··· 197 204 <p className="text-xs">{t("activity:comment.edit")}</p> 198 205 </TooltipContent> 199 206 </Tooltip> 200 - </TooltipProvider> 201 - )} 207 + )} 208 + 209 + <div className="pt-0.5"> 210 + <CommentEditor 211 + value={isEditing ? editedContent : content} 212 + onChange={isEditing ? setEditedContent : undefined} 213 + placeholder={t("activity:comment.editPlaceholder")} 214 + taskId={taskId} 215 + uploadSurface="comment" 216 + className={ 217 + isEditing 218 + ? "[&_.kaneo-comment-editor-content_.ProseMirror]:min-h-[3rem] [&_.kaneo-comment-editor-content_.ProseMirror]:max-h-none [&_.kaneo-comment-editor-content_.ProseMirror]:overflow-visible [&_.kaneo-comment-editor-content_.ProseMirror]:px-3 [&_.kaneo-comment-editor-content_.ProseMirror]:pt-2.5 [&_.kaneo-comment-editor-content_.ProseMirror]:pb-2" 219 + : "kaneo-comment-viewer [&_.kaneo-comment-editor-content_.ProseMirror]:px-3 [&_.kaneo-comment-editor-content_.ProseMirror]:pt-2 [&_.kaneo-comment-editor-content_.ProseMirror]:pb-3" 220 + } 221 + autoFocus={isEditing} 222 + readOnly={!isEditing} 223 + onSubmitShortcut={isEditing ? handleSave : undefined} 224 + onCancelShortcut={isEditing ? handleCancel : undefined} 225 + /> 226 + </div> 202 227 203 - <div className="pt-0.5"> 204 - <CommentEditor 205 - value={isEditing ? editedContent : content} 206 - onChange={isEditing ? setEditedContent : undefined} 207 - placeholder={t("activity:comment.editPlaceholder")} 208 - taskId={taskId} 209 - uploadSurface="comment" 210 - className={ 211 - isEditing 212 - ? "[&_.kaneo-comment-editor-content_.ProseMirror]:min-h-[3rem] [&_.kaneo-comment-editor-content_.ProseMirror]:max-h-none [&_.kaneo-comment-editor-content_.ProseMirror]:overflow-visible [&_.kaneo-comment-editor-content_.ProseMirror]:px-3 [&_.kaneo-comment-editor-content_.ProseMirror]:pt-2.5 [&_.kaneo-comment-editor-content_.ProseMirror]:pb-2" 213 - : "kaneo-comment-viewer [&_.kaneo-comment-editor-content_.ProseMirror]:px-3 [&_.kaneo-comment-editor-content_.ProseMirror]:pt-2 [&_.kaneo-comment-editor-content_.ProseMirror]:pb-3" 214 - } 215 - autoFocus={isEditing} 216 - readOnly={!isEditing} 217 - onSubmitShortcut={isEditing ? handleSave : undefined} 218 - onCancelShortcut={isEditing ? handleCancel : undefined} 219 - /> 228 + {isEditing && ( 229 + <div className="flex items-center justify-end gap-2 border-border/70 border-t bg-card/60 px-3 py-2"> 230 + <Button 231 + variant="secondary" 232 + size="sm" 233 + onClick={handleCancel} 234 + disabled={isPending} 235 + className="h-7 px-2.5 text-xs" 236 + > 237 + {t("common:actions.cancel")} 238 + </Button> 239 + <Button 240 + variant="default" 241 + size="sm" 242 + onClick={handleSave} 243 + disabled={isPending || !editedContent.trim()} 244 + className="h-7 px-2.5 text-xs" 245 + > 246 + {t("activity:comment.save")} 247 + </Button> 248 + </div> 249 + )} 220 250 </div> 221 - 222 - {isEditing && ( 223 - <div className="flex items-center justify-end gap-2 border-border/70 border-t bg-card/60 px-3 py-2"> 224 - <Button 225 - variant="secondary" 226 - size="sm" 227 - onClick={handleCancel} 228 - disabled={isPending} 229 - className="h-7 px-2.5 text-xs" 230 - > 231 - {t("common:actions.cancel")} 232 - </Button> 233 - <Button 234 - variant="default" 235 - size="sm" 236 - onClick={handleSave} 237 - disabled={isPending || !editedContent.trim()} 238 - className="h-7 px-2.5 text-xs" 239 - > 240 - {t("activity:comment.save")} 241 - </Button> 242 - </div> 243 - )} 244 - </div> 251 + </TooltipProvider> 245 252 ); 246 253 }
+12
apps/web/src/lib/format.ts
··· 43 43 ); 44 44 } 45 45 46 + /** Locale-aware full date and time (for tooltips next to relative labels like "yesterday"). */ 47 + export function formatDateTime(value: DateInput, locale?: string) { 48 + return formatDate( 49 + value, 50 + { 51 + dateStyle: "medium", 52 + timeStyle: "short", 53 + }, 54 + locale, 55 + ); 56 + } 57 + 46 58 export function formatRelativeTime( 47 59 value: DateInput, 48 60 locale?: string,