Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions frontend/src/components/home/GoalsCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,85 @@
backdrop-filter: blur(4px);
}
}

/* Loading skeleton */
.goals-card--loading {
cursor: default;
pointer-events: none;
}

.goals-card__skeleton {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}

.goals-card__skeleton-header {
width: 40%;
height: 0.9em;
background: linear-gradient(
90deg,
var(--color-surface) 25%,
var(--glass-border) 50%,
var(--color-surface) 75%
);
background-size: 200% 100%;
border-radius: var(--radius-sm);
animation: goals-shimmer 1.5s infinite;
}

.goals-card__skeleton-line {
width: 100%;
height: 0.9em;
background: linear-gradient(
90deg,
var(--color-surface) 25%,
var(--glass-border) 50%,
var(--color-surface) 75%
);
background-size: 200% 100%;
border-radius: var(--radius-sm);
animation: goals-shimmer 1.5s infinite;
}

.goals-card__skeleton-line--short {
width: 70%;
}

@keyframes goals-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}

/* Entrance animation */
@keyframes goals-enter {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}

.goals-card--enter {
animation: goals-enter 0.2s ease backwards;
}

/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.goals-card__skeleton-header,
.goals-card__skeleton-line {
animation: none;
background: var(--glass-border);
}

.goals-card--enter {
animation: none;
}
}
28 changes: 25 additions & 3 deletions frontend/src/components/home/GoalsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import remarkGfm from "remark-gfm";
import { useSession } from "../../contexts/SessionContext";
import "./GoalsCard.css";

/**
* Props for GoalsCard component.
*/
export interface GoalsCardProps {
/** Whether goals are still loading */
isLoading?: boolean;
}

/**
* GoalsCard displays goals.md content as rendered markdown.
*
Expand All @@ -19,23 +27,37 @@ import "./GoalsCard.css";
* - Returns null if no goals file exists in the vault
* - Clicking the card triggers /review-goals command
*/
export function GoalsCard(): React.ReactNode {
export function GoalsCard({ isLoading = false }: GoalsCardProps): React.ReactNode {
const { goals, setDiscussionPrefill, setMode } = useSession();

const handleClick = useCallback(() => {
setDiscussionPrefill("/review-goals");
setMode("discussion");
}, [setDiscussionPrefill, setMode]);

// Don't render if no goals content
// Show skeleton during load
if (isLoading) {
return (
<div className="goals-card goals-card--loading" aria-label="Goals loading">
<div className="goals-card__skeleton">
<div className="goals-card__skeleton-header" />
<div className="goals-card__skeleton-line" />
<div className="goals-card__skeleton-line goals-card__skeleton-line--short" />
<div className="goals-card__skeleton-line" />
</div>
</div>
);
}

// Don't render if no goals content (vault might not have goalsPath)
if (!goals) {
return null;
}

return (
<button
type="button"
className="goals-card"
className="goals-card goals-card--enter"
onClick={handleClick}
aria-label="Review goals"
>
Expand Down
96 changes: 59 additions & 37 deletions frontend/src/components/home/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,12 @@ export function HomeView(): React.ReactNode {
const { getGoals, getInspiration } = useHome(vault?.id);
const { deleteSession } = useSessions(vault?.id);

// Inspiration state
// Loading states
const [inspirationLoading, setInspirationLoading] = useState(true);
const [goalsLoading, setGoalsLoading] = useState(true);
const [activityLoading, setActivityLoading] = useState(true);

// Inspiration state
const [inspirationContextual, setInspirationContextual] =
useState<InspirationItem | null>(null);
const [inspirationQuote, setInspirationQuote] =
Expand Down Expand Up @@ -154,38 +158,58 @@ export function HomeView(): React.ReactNode {
const vaultId = vault.id;
console.log(`[HomeView] Loading data for vault: ${vaultId}`);

// Reset loading states for new vault
setActivityLoading(true);
setGoalsLoading(true);
setInspirationLoading(true);

// Load recent activity
getRecentActivity().then((activity) => {
console.log(`[HomeView] Recent activity loaded:`, activity);
if (activity) {
setRecentNotes(activity.captures);
setRecentDiscussions(activity.discussions);
}
}).catch((err) => console.error(`[HomeView] Failed to load activity:`, err));
getRecentActivity()
.then((activity) => {
console.log(`[HomeView] Recent activity loaded:`, activity);
if (activity) {
setRecentNotes(activity.captures);
setRecentDiscussions(activity.discussions);
}
setActivityLoading(false);
})
.catch((err) => {
console.error(`[HomeView] Failed to load activity:`, err);
setActivityLoading(false);
});

// Load goals (only if vault has goalsPath)
if (vault.goalsPath) {
getGoals().then((content) => {
console.log(`[HomeView] Goals loaded:`, content?.slice(0, 50));
if (content !== null) {
setGoals(content);
}
}).catch((err) => console.error(`[HomeView] Failed to load goals:`, err));
getGoals()
.then((content) => {
console.log(`[HomeView] Goals loaded:`, content?.slice(0, 50));
if (content !== null) {
setGoals(content);
}
setGoalsLoading(false);
})
.catch((err) => {
console.error(`[HomeView] Failed to load goals:`, err);
setGoalsLoading(false);
});
} else {
setGoalsLoading(false);
}

// Load inspiration
setInspirationLoading(true);
getInspiration().then((result) => {
console.log(`[HomeView] Inspiration loaded:`, result);
if (result) {
setInspirationContextual(result.contextual);
setInspirationQuote(result.quote);
}
setInspirationLoading(false);
}).catch((err) => {
console.error(`[HomeView] Failed to load inspiration:`, err);
setInspirationLoading(false);
});
getInspiration()
.then((result) => {
console.log(`[HomeView] Inspiration loaded:`, result);
if (result) {
setInspirationContextual(result.contextual);
setInspirationQuote(result.quote);
}
setInspirationLoading(false);
})
.catch((err) => {
console.error(`[HomeView] Failed to load inspiration:`, err);
setInspirationLoading(false);
});
}, [vault?.id, getRecentActivity, getGoals, getInspiration, setRecentNotes, setRecentDiscussions, setGoals]);

// Determine which debrief buttons to show (single Date for consistency)
Expand Down Expand Up @@ -234,23 +258,21 @@ export function HomeView(): React.ReactNode {
)}
</section>

{/* Inspiration */}
{inspirationQuote && (
<InspirationCard
contextual={inspirationContextual}
quote={inspirationQuote}
isLoading={inspirationLoading}
/>
)}
{/* Inspiration - always rendered, shows skeleton when loading */}
<InspirationCard
contextual={inspirationContextual}
quote={inspirationQuote}
isLoading={inspirationLoading}
/>

{/* Spaced Repetition - renders null internally when no cards due */}
{/* Spaced Repetition - shows idle state when no cards due */}
<SpacedRepetitionWidget vaultId={vault?.id} />

{/* Goals */}
<GoalsCard />
<GoalsCard isLoading={goalsLoading} />

{/* Recent Activity */}
<RecentActivity onDeleteSession={handleDeleteSession} />
<RecentActivity isLoading={activityLoading} onDeleteSession={handleDeleteSession} />

{/* Health Issues */}
<HealthPanel />
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/components/home/InspirationCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,22 @@
}
}

/* Entrance animation */
@keyframes inspiration-enter {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}

.inspiration-card__item--enter {
animation: inspiration-enter 0.2s ease backwards;
}

/* Fallback for browsers without backdrop-filter */
@supports not (backdrop-filter: blur(10px)) {
.inspiration-card__item {
Expand All @@ -172,6 +188,10 @@
animation: none;
background: var(--glass-border);
}

.inspiration-card__item--enter {
animation: none;
}
}

/* Touch device adjustments */
Expand Down
11 changes: 6 additions & 5 deletions frontend/src/components/home/InspirationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import "./InspirationCard.css";
export interface InspirationCardProps {
/** Contextual prompt (null if not available or on weekends) */
contextual: InspirationItem | null;
/** Inspirational quote (always present) */
quote: InspirationItem;
/** Inspirational quote (null during loading or if unavailable) */
quote: InspirationItem | null;
/** Whether data is still loading */
isLoading?: boolean;
}
Expand All @@ -44,7 +44,8 @@ export function InspirationCard({
[setDiscussionPrefill, setMode]
);

if (isLoading) {
// Show skeleton if loading or no quote available
if (isLoading || !quote) {
return (
<section className="inspiration-card inspiration-card--loading" aria-label="Inspiration">
<div className="inspiration-card__skeleton">
Expand All @@ -59,7 +60,7 @@ export function InspirationCard({
<section className="inspiration-card" aria-label="Inspiration">
<button
type="button"
className="inspiration-card__item inspiration-card__quote"
className="inspiration-card__item inspiration-card__quote inspiration-card__item--enter"
onClick={() => handleClick(quote.text)}
aria-label="Use this quote for discussion"
>
Expand All @@ -74,7 +75,7 @@ export function InspirationCard({
{contextual && (
<button
type="button"
className="inspiration-card__item inspiration-card__prompt"
className="inspiration-card__item inspiration-card__prompt inspiration-card__item--enter"
onClick={() => handleClick(contextual.text)}
aria-label="Use this prompt for discussion"
>
Expand Down
Loading