Skip to content

Commit 63b652b

Browse files
Basically done with the home page. left with the content for the pseudo sidebar and then the download functionality, and then we are done with it. afterwards, the player page/modal and then the edit before download modal.
1 parent d080707 commit 63b652b

File tree

9 files changed

+200
-57
lines changed

9 files changed

+200
-57
lines changed

app/_layout.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import NavBar from "../components/NavBar";
99
import SideBar from "../components/SideBar";
1010
import { SidebarProvider } from "../contexts/SidebarContext";
1111
import { SearchProvider } from "../contexts/SearchContext";
12+
import Toast from "react-native-toast-message";
1213

1314
export default function Layout() {
1415
return (
@@ -17,6 +18,7 @@ export default function Layout() {
1718
<SidebarProvider>
1819
<SearchProvider>
1920
<LayoutContent />
21+
<Toast />
2022
</SearchProvider>
2123
</SidebarProvider>
2224
</AppStorageProvider>

app/index.js

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,65 +5,116 @@ import { useEffect } from "react";
55
import { useAppStorage } from "../contexts/AppStorageContext";
66
import PaginatedResults from "../components/PaginatedResults";
77
import AccordionGroup from "../components/AccordionGroup";
8-
import Icon from "react-native-vector-icons/Ionicons";
98
import { useSearch } from "../contexts/SearchContext";
10-
import { Skeleton } from "../components/Skeleton";
11-
import { HEXA } from "../lib/colors";
129
import LoadingSkeleton from "../components/LoadingSkeleton";
10+
import Toast from "react-native-toast-message";
1311

1412
export default function Home() {
1513
const { setRightSidebarKey } = useRightSidebar();
16-
const { viewMode } = useAppStorage();
17-
const { normalized, isLoading, error, setIsLoading } = useSearch();
14+
const { viewMode, lastSearch, setLastSearch } = useAppStorage();
15+
const { normalized, isLoading, error } = useSearch();
16+
const { theme, themeMode, accentKey, accentColors } = useTheme();
1817

18+
// Save the *normalized* result to storage when it becomes available.
19+
// (We intentionally DO NOT write lastSearch when it's null/undefined)
20+
useEffect(() => {
21+
if (!normalized) return;
22+
// do a cheap equality check — stringify is simple and enough here
23+
try {
24+
const normStr = JSON.stringify(normalized);
25+
const lastStr = JSON.stringify(lastSearch);
26+
if (normStr === lastStr) return; // no change -> don't write
27+
} catch (e) {
28+
// fallback: if stringify fails, still write once
29+
}
30+
setLastSearch(normalized);
31+
}, [normalized, lastSearch, setLastSearch]);
32+
33+
// set right sidebar key for this page
1934
useEffect(() => {
2035
setRightSidebarKey("home");
2136
return () => setRightSidebarKey(null);
22-
}, []);
37+
}, [setRightSidebarKey]);
38+
39+
// show toast when there's an error (still allow fallback rendering)
40+
useEffect(() => {
41+
if (error) {
42+
Toast.show({
43+
type: "error",
44+
position: "top",
45+
text1: "Error",
46+
text2: String(error),
47+
text2Style: { fontSize: 16 },
48+
visibilityTime: 4000,
49+
autoHide: true,
50+
});
51+
}
52+
}, [error]);
2353

24-
// setIsLoading(true);
54+
// Determine source to render: prefer live normalized results, otherwise use lastSearch
55+
const source = normalized ?? lastSearch ?? null;
56+
const displayType = normalized?.type ?? lastSearch?.type ?? null;
2557

26-
if (!isLoading && !error && !normalized) {
58+
// Loading state (use displayType to let skeleton adjust)
59+
if (isLoading) {
60+
return (
61+
<ScrollView
62+
showsVerticalScrollIndicator={false}
63+
style={[styles.container]}
64+
>
65+
<LoadingSkeleton viewMode={viewMode} displayType={displayType} />
66+
</ScrollView>
67+
);
68+
}
69+
70+
// No results at all (initial empty state)
71+
if (!source) {
2772
return (
2873
<View style={[styles.container, { alignItems: "center", marginTop: 40 }]}>
29-
<Icon name="search-off" size={48} color="#999" />
30-
<Text>Begin Search</Text>
74+
<Text
75+
style={[
76+
{
77+
color: theme.textSecondary,
78+
fontWeight: "bold",
79+
fontSize: 40,
80+
letterSpacing: 3,
81+
},
82+
]}
83+
>
84+
Your Search Results Appear Here.
85+
</Text>
3186
</View>
3287
);
3388
}
89+
90+
// If we have a source (normalized or lastSearch) — render it.
91+
// It should match the shape you expect (bulk vs single)
3492
return (
3593
<ScrollView showsVerticalScrollIndicator={false} style={[styles.container]}>
36-
{}
37-
{}
38-
{isLoading ? (
39-
<LoadingSkeleton viewMode={viewMode} displayType={normalized?.type} />
40-
) : error ? (
41-
<Text style={{ color: "red" }}>{error}</Text>
42-
) : !isLoading && !normalized ? (
43-
<Text>Enter a query and press Enter</Text>
44-
) : (
45-
<View>
46-
{/* <ScrollView style={{ padding: 16 }}> */}
47-
48-
{normalized?.type === "bulk" ? (
49-
normalized?.blocks.map((block) => (
50-
<AccordionGroup
51-
key={block.search_term.query}
52-
block={block}
53-
pageSize={9}
54-
viewMode={viewMode}
55-
/>
56-
))
57-
) : (
58-
<PaginatedResults
59-
style={styles.searchResults}
60-
songs={normalized?.items}
61-
pageSize={12}
94+
<View>
95+
{source.type === "bulk" ? (
96+
// bulk blocks: if lastSearch was saved it should have the same `blocks` structure
97+
source.blocks?.map((block) => (
98+
<AccordionGroup
99+
key={
100+
block.search_term?.query ??
101+
Math.random().toString(36).slice(2, 9)
102+
}
103+
block={block}
104+
pageSize={9}
62105
viewMode={viewMode}
63106
/>
64-
)}
65-
</View>
66-
)}
107+
))
108+
) : (
109+
// single search: normalized.items or lastSearch.items
110+
<PaginatedResults
111+
style={styles.searchResults}
112+
songs={source.items ?? []}
113+
pageSize={12}
114+
viewMode={viewMode}
115+
/>
116+
)}
117+
</View>
67118
</ScrollView>
68119
);
69120
}

app/settings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export default function Settings() {
106106
accent={theme.accent}
107107
accentLight={accentColors[accentKey].light}
108108
accentDark={accentColors[accentKey].dark}
109-
/>{" "}
109+
/>
110110
</View>
111111
</View>
112112
</View>

components/PaginatedResults.jsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export default function PaginatedResults({
5353
const goPrev = () => goto(page - 1);
5454
const goNext = () => goto(page + 1);
5555

56+
const onDownload = (song) => {
57+
console.log("This would be Downloaded: ", song);
58+
};
59+
5660
return (
5761
<View style={[{ width: "100%" }]}>
5862
{/* Render the current page items using your exact markup */}
@@ -189,7 +193,7 @@ export default function PaginatedResults({
189193
Play
190194
</Text>
191195
</Pressable>
192-
</View>{" "}
196+
</View>
193197
<View
194198
style={[
195199
styles?.buttonBox,
@@ -202,7 +206,10 @@ export default function PaginatedResults({
202206
},
203207
]}
204208
>
205-
<Pressable style={[styles?.button]}>
209+
<Pressable
210+
onPress={() => onDownload(song.webpage_url)}
211+
style={[styles?.button]}
212+
>
206213
<Text
207214
style={[
208215
{
@@ -338,7 +345,7 @@ export default function PaginatedResults({
338345
Play
339346
</Text>
340347
</Pressable>
341-
</View>{" "}
348+
</View>
342349
<View
343350
style={[
344351
styles?.buttonBoxGrid,
@@ -351,7 +358,10 @@ export default function PaginatedResults({
351358
},
352359
]}
353360
>
354-
<Pressable style={[styles?.button]}>
361+
<Pressable
362+
onPress={() => onDownload(song.webpage_url)}
363+
style={[styles?.button]}
364+
>
355365
<Text
356366
style={[
357367
{

components/Skeleton.jsx

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
import React, { useEffect, useRef } from "react";
33
import { View, Animated, StyleSheet } from "react-native";
44

5+
/**
6+
* Skeleton shimmer. Starts/stops based on `isLoading`.
7+
*
8+
* Keep using the same props as before. Use it like:
9+
* <Skeleton width={300} height={18} isLoading={isLoading} />
10+
*/
511
export const Skeleton = ({
612
width = 100,
713
height = 20,
@@ -10,31 +16,77 @@ export const Skeleton = ({
1016
highlightColor = "#F2F8FC",
1117
duration = 1200,
1218
style,
19+
isLoading = true, // pass your search-context isLoading here
1320
}) => {
1421
const shimmer = useRef(new Animated.Value(0)).current;
22+
const loopRef = useRef(null);
23+
24+
// highlight bar width (bigger than container so it sweeps fully)
25+
const highlightWidth = Math.max(width * 1.6, 80);
1526

1627
useEffect(() => {
17-
shimmer.setValue(0);
18-
Animated.loop(
19-
Animated.timing(shimmer, {
28+
// helper to start the infinite loop
29+
const startLoop = () => {
30+
// reset to 0 so animation always starts at left
31+
shimmer.setValue(0);
32+
33+
const timing = Animated.timing(shimmer, {
2034
toValue: 1,
21-
duration,
22-
// useNativeDriver: true,
23-
})
24-
).start();
25-
}, [shimmer, duration]);
35+
duration: Math.max(300, duration),
36+
useNativeDriver: false, // explicitly false for web compatibility
37+
});
38+
39+
// store the loop so we can stop it later
40+
loopRef.current = Animated.loop(timing);
41+
loopRef.current.start();
42+
};
43+
44+
// helper to stop the loop
45+
const stopLoop = () => {
46+
if (loopRef.current && typeof loopRef.current.stop === "function") {
47+
try {
48+
loopRef.current.stop();
49+
} catch (e) {
50+
/* ignore */
51+
}
52+
loopRef.current = null;
53+
}
54+
// reset shimmer position visually
55+
try {
56+
shimmer.setValue(0);
57+
} catch (e) {
58+
/* ignore */
59+
}
60+
};
2661

62+
if (isLoading) startLoop();
63+
else stopLoop();
64+
65+
return () => {
66+
// cleanup on unmount
67+
stopLoop();
68+
};
69+
}, [isLoading, duration, shimmer, width, highlightWidth]);
70+
71+
// translate from -highlightWidth to +width (left -> right)
2772
const translateX = shimmer.interpolate({
2873
inputRange: [0, 1],
29-
outputRange: [-width, width], // left → right only
74+
outputRange: [-highlightWidth, width],
3075
});
3176

3277
return (
3378
<View
3479
style={[
35-
{ width, height, borderRadius, backgroundColor, overflow: "hidden" },
80+
{
81+
width,
82+
height,
83+
borderRadius,
84+
backgroundColor,
85+
overflow: "hidden",
86+
},
3687
style,
3788
]}
89+
pointerEvents="none"
3890
>
3991
<Animated.View
4092
style={[
@@ -43,11 +95,13 @@ export const Skeleton = ({
4395
background: `linear-gradient(90deg, ${backgroundColor}, ${highlightColor}, ${backgroundColor} )`,
4496
filter: "blur(8px)",
4597
opacity: 0.4,
46-
width,
98+
highlightWidth,
4799
transform: [{ translateX }],
48100
},
49101
]}
50102
/>
51103
</View>
52104
);
53105
};
106+
107+
export default Skeleton;

contexts/AppStorageContext.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export function AppStorageProvider({ children }) {
1717
// new flag - whether to save incoming searches
1818
saveSearchHistory: true,
1919
viewMode: "list",
20+
// last cached search (trimmed)
21+
lastSearch: null, // { type: 'single'|'bulk', query?, queries?, items: [...], when }
2022
};
2123

2224
const [data, setData] = useState(defaultData);
@@ -94,7 +96,13 @@ export function AppStorageProvider({ children }) {
9496
viewMode: data.viewMode === "list" ? "grid" : "list",
9597
}));
9698

97-
const getViewMode = () => data.viewMode || "list";
99+
// NEW: lastSearch helpers (store and retrieve the normalized response as-is)
100+
const setLastSearch = (payload) =>
101+
setData((s) => ({ ...s, lastSearch: payload || null }));
102+
103+
const getLastSearch = () => data.lastSearch || null;
104+
105+
const clearLastSearch = () => setData((s) => ({ ...s, lastSearch: null }));
98106

99107
const setPlaylists = (playlists) => setData((s) => ({ ...s, playlists }));
100108

@@ -124,6 +132,12 @@ export function AppStorageProvider({ children }) {
124132
setPlaylists,
125133
// (optionally) setter for raw data
126134
setData,
135+
136+
// last search cache
137+
setLastSearch,
138+
lastSearch: data.lastSearch,
139+
getLastSearch,
140+
clearLastSearch,
127141
}}
128142
>
129143
{children}

contexts/SearchContext.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function SearchProvider({ children, defaultPageSize = 12 }) {
5757

5858
try {
5959
// NOTE: backend expects `?query=...`
60-
const url = `https://seekbeat.onrender.com/api/seargch/?query=${encodeURIComponent(
60+
const url = `https://seekbeat.onrender.com/api/search/?query=${encodeURIComponent(
6161
q
6262
)}`;
6363
const res = await fetch(url, { signal: controller.signal });

0 commit comments

Comments
 (0)