made somewhat ok ui i guess but still need to add restapi logic and im still missing a home/login page, also need to test out if it work on web?? i guess it should but will have the mobile ui but who cares lol

This commit is contained in:
Ksan 2026-04-27 13:30:39 +02:00
parent 97aea8e940
commit ac219aab0b
24 changed files with 2287 additions and 1159 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/etfoglasi-frontend.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/etfoglasi-frontend.iml" filepath="$PROJECT_DIR$/.idea/etfoglasi-frontend.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -47,3 +47,4 @@
} }
} }
} }

59
app/(tabs)/_layout.tsx Normal file
View File

@ -0,0 +1,59 @@
import { Tabs } from "expo-router";
import { useColorScheme } from "react-native";
import { Ionicons } from "@expo/vector-icons";
const LIGHT = { bg: "#FFFFFF", border: "#E8E2D5", active: "#C4622D", inactive: "#8A8278" };
const DARK = { bg: "#161513", border: "#2C2A27", active: "#E07B45", inactive: "#706D67" };
export default function TabLayout() {
const scheme = useColorScheme();
const dark = scheme === "dark";
const c = dark ? DARK : LIGHT;
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: {
backgroundColor: c.bg,
borderTopColor: c.border,
borderTopWidth: 1,
height: 64,
paddingBottom: 10,
paddingTop: 6,
},
tabBarActiveTintColor: c.active,
tabBarInactiveTintColor: c.inactive,
tabBarLabelStyle: { fontSize: 11, fontWeight: "600" },
}}
>
<Tabs.Screen
name="index"
options={{
title: "Subscribed",
tabBarIcon: ({ focused, color, size }) => (
<Ionicons name={focused ? "bookmark" : "bookmark-outline"} size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="all"
options={{
title: "All",
tabBarIcon: ({ focused, color, size }) => (
<Ionicons name={focused ? "compass" : "compass-outline"} size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profile",
tabBarIcon: ({ focused, color, size }) => (
<Ionicons name={focused ? "person-circle" : "person-circle-outline"} size={size} color={color} />
),
}}
/>
</Tabs>
);
}

2
app/(tabs)/all.tsx Normal file
View File

@ -0,0 +1,2 @@
import AllFeed from "@/screens/AllFeed";
export default AllFeed;

3
app/(tabs)/index.tsx Normal file
View File

@ -0,0 +1,3 @@
import SubscribedFeed from "@/screens/SubscribedFeed";
export default SubscribedFeed;

2
app/(tabs)/profile.tsx Normal file
View File

@ -0,0 +1,2 @@
import Profile from "@/screens/Profile";
export default Profile;

View File

@ -1,6 +1,6 @@
import "../globals.css";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import './globals.css'
export default function RootLayout() { export default function RootLayout() {
return <Stack />; return <Stack screenOptions={{ headerShown: false }} />;
} }

View File

@ -1,10 +0,0 @@
import { Text, View } from "react-native";
export default function Index() {
return (
<View className="flex-1 justify-center items-center">
<Text className="text-5xl text-primary font-bold text-center">Test </Text>
</View>
);
}

View File

@ -0,0 +1,111 @@
import React, { useState } from "react";
import {
View,
Text,
TouchableOpacity,
Animated,
LayoutAnimation,
Platform,
UIManager,
useColorScheme,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import ExpandableItem, { FeedItem } from "./ExpandableItem";
if (Platform.OS === "android") {
UIManager.setLayoutAnimationEnabledExperimental?.(true);
}
export interface Category {
id: string;
label: string;
icon: keyof typeof Ionicons.glyphMap;
color: string; // accent hex for the icon badge
darkColor: string;
items: FeedItem[];
}
interface CollapsibleCategoryProps {
category: Category;
defaultOpen?: boolean;
}
export default function CollapsibleCategory({
category,
defaultOpen = false,
}: CollapsibleCategoryProps) {
const [open, setOpen] = useState(defaultOpen);
const scheme = useColorScheme();
const dark = scheme === "dark";
const accentColor = dark ? category.darkColor : category.color;
const toggle = () => {
LayoutAnimation.configureNext({
duration: 260,
create: { type: "easeInEaseOut", property: "opacity" },
update: { type: "spring", springDamping: 0.85 },
});
setOpen((v) => !v);
};
return (
<View className="mb-4">
<TouchableOpacity
onPress={toggle}
activeOpacity={0.8}
className={`
mx-4 flex-row items-center px-4 py-3.5 rounded-2xl
${dark
? "bg-obsidian-50 border border-border-dark"
: "bg-parchment-100 border border-border-light"
}
`}
>
{/* Icon badge */}
<View
className="w-8 h-8 rounded-xl items-center justify-center mr-3"
style={{ backgroundColor: accentColor + "25" }}
>
<Ionicons name={category.icon} size={16} color={accentColor} />
</View>
<Text
className={`flex-1 text-sm font-sans font-bold tracking-wide ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
>
{category.label}
</Text>
{/* Count badge */}
<View
className="px-2 py-0.5 rounded-full mr-3"
style={{ backgroundColor: accentColor + "20" }}
>
<Text
className="text-xs font-sans font-semibold"
style={{ color: accentColor }}
>
{category.items.length}
</Text>
</View>
<Ionicons
name={open ? "chevron-up" : "chevron-down"}
size={16}
color={dark ? "#706D67" : "#8A8278"}
/>
</TouchableOpacity>
{/* Items */}
{open && (
<View className="mt-2">
{category.items.map((item) => (
<ExpandableItem key={item.id} item={item} />
))}
</View>
)}
</View>
);
}

View File

@ -0,0 +1,153 @@
import React, { useState, useRef } from "react";
import {
View,
Text,
TouchableOpacity,
Animated,
LayoutAnimation,
Platform,
UIManager,
useColorScheme,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
// Enable LayoutAnimation on Android idk honestly probably ok?
if (Platform.OS === "android") {
UIManager.setLayoutAnimationEnabledExperimental?.(true);
}
export interface FeedItem {
id: string;
title: string;
author: string;
timestamp: string;
tag: string;
paragraphs: string[];
}
interface ExpandableItemProps {
item: FeedItem;
}
export default function ExpandableItem({ item }: ExpandableItemProps) {
const [expanded, setExpanded] = useState(false);
const rotateAnim = useRef(new Animated.Value(0)).current;
const scheme = useColorScheme();
const dark = scheme === "dark";
const toggle = () => {
LayoutAnimation.configureNext({
duration: 280,
create: { type: "easeInEaseOut", property: "opacity" },
update: { type: "spring", springDamping: 0.8 },
});
Animated.timing(rotateAnim, {
toValue: expanded ? 0 : 1,
duration: 250,
useNativeDriver: true,
}).start();
setExpanded((v) => !v);
};
const chevronRotation = rotateAnim.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "180deg"],
});
return (
<TouchableOpacity
activeOpacity={0.85}
onPress={toggle}
className={`
mx-4 mb-3 rounded-2xl overflow-hidden
${dark
? "bg-obsidian-100 border border-border-dark"
: "bg-surface-light border border-border-light"
}
`}
style={{
shadowColor: dark ? "#000" : "#1A1714",
shadowOpacity: dark ? 0.35 : 0.06,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 8,
elevation: 2,
}}
>
{/* Header */}
<View className="flex-row items-center px-4 py-4">
<View className="flex-1 pr-3">
{/* Tag + timestamp row */}
<View className="flex-row items-center mb-1 gap-2">
<View
className={`px-2 py-0.5 rounded-full ${
dark ? "bg-accent-dark/20" : "bg-accent/10"
}`}
>
<Text
className={`text-xs font-sans font-semibold tracking-wide ${
dark ? "text-accent-dark" : "text-accent"
}`}
>
{item.tag}
</Text>
</View>
<Text
className={`text-xs ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
{item.timestamp}
</Text>
</View>
{/* Title */}
<Text
className={`text-base font-sans font-semibold leading-snug ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
numberOfLines={expanded ? undefined : 2}
>
{item.title}
</Text>
{/* Author */}
<Text
className={`text-xs mt-1 ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
by {item.author}
</Text>
</View>
<Animated.View style={{ transform: [{ rotate: chevronRotation }] }}>
<Ionicons
name="chevron-down"
size={18}
color={dark ? "#706D67" : "#8A8278"}
/>
</Animated.View>
</View>
{/* Expanded body */}
{expanded && (
<View
className={`px-4 pb-5 pt-1 border-t ${
dark ? "border-border-dark" : "border-border-light"
}`}
>
{item.paragraphs.map((para, i) => (
<Text
key={i}
className={`text-sm font-sans leading-relaxed mb-3 last:mb-0 ${
dark ? "text-ink-dark/80" : "text-ink-light/80"
}`}
>
{para}
</Text>
))}
</View>
)}
</TouchableOpacity>
);
}

66
components/SearchBar.tsx Normal file
View File

@ -0,0 +1,66 @@
import React, { useState } from "react";
import {
View,
TextInput,
TouchableOpacity,
useColorScheme,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
interface SearchBarProps {
placeholder?: string;
onSearch?: (text: string) => void;
}
export default function SearchBar({
placeholder = "Search…",
onSearch,
}: SearchBarProps) {
const [query, setQuery] = useState("");
const scheme = useColorScheme();
const dark = scheme === "dark";
const handleChange = (text: string) => {
setQuery(text);
onSearch?.(text);
};
return (
<View
className={`
flex-row items-center mx-4 my-3 px-4 rounded-2xl h-11
${dark
? "bg-obsidian-100 border border-border-dark"
: "bg-parchment-100 border border-border-light"
}
`}
>
<Ionicons
name="search"
size={16}
color={dark ? "#706D67" : "#8A8278"}
style={{ marginRight: 8 }}
/>
<TextInput
className={`flex-1 text-sm font-sans ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
placeholderTextColor={dark ? "#706D67" : "#8A8278"}
placeholder={placeholder}
value={query}
onChangeText={handleChange}
returnKeyType="search"
clearButtonMode="while-editing"
/>
{query.length > 0 && (
<TouchableOpacity onPress={() => handleChange("")} hitSlop={8}>
<Ionicons
name="close-circle"
size={16}
color={dark ? "#706D67" : "#8A8278"}
/>
</TouchableOpacity>
)}
</View>
);
}

196
data/mockData.ts Normal file
View File

@ -0,0 +1,196 @@
import { FeedItem } from "@/components/ExpandableItem";
import { Category } from "@/components/CollapsibleCategory";
// random generated data to check the UI before I connect to the server and fetch data
// ── 1st Year Subjects ───────────────────────────────────────────────────────
const YEAR_1_SUBJECTS = [
{
id: "y1-maths",
title: "Mathematics I",
author: "Prof. John Doe",
timestamp: "2 hours ago",
tag: "Mathematics",
paragraphs: [
"Introduction to Calculus, Linear Algebra, and Differential Equations.",
"Topics include Limits, Continuity, and Integration techniques."
]
},
{
id: "y1-physics",
title: "Physics I",
author: "Prof. Jane Smith",
timestamp: "5 hours ago",
tag: "Physics",
paragraphs: [
"Fundamentals of Mechanics, Thermodynamics, and Waves.",
"Topics include Newtons Laws, Energy, and Thermodynamics."
]
},
];
// ── 2nd Year Subjects ───────────────────────────────────────────────────────
const YEAR_2_SUBJECTS = [
{
id: "y2-maths",
title: "Mathematics II",
author: "Prof. Alan Walker",
timestamp: "1 day ago",
tag: "Mathematics",
paragraphs: [
"Advanced Calculus, Linear Algebra, and Complex Analysis.",
"Topics include Eigenvalues, Eigenvectors, and Fourier Series."
]
},
{
id: "y2-electronics",
title: "Basic Electronics",
author: "Prof. Emma Stone",
timestamp: "Yesterday",
tag: "Electronics",
paragraphs: [
"Introduction to basic electrical circuits, components, and semiconductor devices.",
"Topics include Resistors, Capacitors, Diodes, and Transistors."
]
},
];
// ── 3rd Year Subjects ───────────────────────────────────────────────────────
const YEAR_3_SUBJECTS = [
{
id: "y3-maths",
title: "Mathematics III",
author: "Prof. Robert Brown",
timestamp: "3 days ago",
tag: "Mathematics",
paragraphs: [
"Partial Differential Equations, Laplace Transforms, and Numerical Methods.",
"Topics include PDEs, Laplace Transforms, and Optimization Techniques."
]
},
{
id: "y3-physics",
title: "Physics III",
author: "Prof. Alice White",
timestamp: "Yesterday",
tag: "Physics",
paragraphs: [
"Quantum Mechanics, Relativity, and Solid-State Physics.",
"Topics include Schrödingers Equation, Quantum States, and Relativity."
]
},
];
// ── 4th Year Subjects ───────────────────────────────────────────────────────
const YEAR_4_SUBJECTS = [
{
id: "y4-electronics",
title: "Electronics Engineering Design",
author: "Prof. Kate Johnson",
timestamp: "1 week ago",
tag: "Electronics",
paragraphs: [
"Project-based learning and design of electronic systems.",
"Topics include Embedded Systems, PCB Design, and Signal Processing."
]
},
{
id: "y4-physics",
title: "Physics IV",
author: "Prof. Michael Harris",
timestamp: "5 days ago",
tag: "Physics",
paragraphs: [
"Advanced topics in Particle Physics, Astrophysics, and Nuclear Physics.",
"Topics include Quantum Field Theory, Astrophysics, and Particle Accelerators."
]
},
];
// ── Subscribed Feed ──────────────────────────────────────────────────────────
export const SUBSCRIBED_ITEMS: FeedItem[] = [
{
id: "s1",
title: "Mathematics I",
author: "Prof. John Doe",
timestamp: "2 hours ago",
tag: "Mathematics",
paragraphs: YEAR_1_SUBJECTS[0].paragraphs,
},
{
id: "s2",
title: "Physics I",
author: "Prof. Jane Smith",
timestamp: "5 hours ago",
tag: "Physics",
paragraphs: YEAR_1_SUBJECTS[1].paragraphs,
},
{
id: "s3",
title: "Mathematics II",
author: "Prof. Alan Walker",
timestamp: "1 day ago",
tag: "Mathematics",
paragraphs: YEAR_2_SUBJECTS[0].paragraphs,
},
{
id: "s4",
title: "Basic Electronics",
author: "Prof. Emma Stone",
timestamp: "Yesterday",
tag: "Electronics",
paragraphs: YEAR_2_SUBJECTS[1].paragraphs,
},
{
id: "s5",
title: "Mathematics III",
author: "Prof. Robert Brown",
timestamp: "3 days ago",
tag: "Mathematics",
paragraphs: YEAR_3_SUBJECTS[0].paragraphs,
},
];
// ── All Feed: Years & Subjects ──────────────────────────────────────────────
export const ALL_CATEGORIES: Category[] = [
{
id: "cat-year-1",
label: "1st Year",
icon: "book-outline",
color: "#3B7DD8",
darkColor: "#5E9EF4",
items: YEAR_1_SUBJECTS,
},
{
id: "cat-year-2",
label: "2nd Year",
icon: "book-outline",
color: "#2E9E6B",
darkColor: "#4EC992",
items: YEAR_2_SUBJECTS,
},
{
id: "cat-year-3",
label: "3rd Year",
icon: "book-outline",
color: "#9B4FB8",
darkColor: "#C47CE0",
items: YEAR_3_SUBJECTS,
},
{
id: "cat-year-4",
label: "4th Year",
icon: "book-outline",
color: "#C4622D",
darkColor: "#E07B45",
items: YEAR_4_SUBJECTS,
},
];

114
index.tsx Normal file
View File

@ -0,0 +1,114 @@
import "./globals.css";
import React from "react";
import { useColorScheme } from "react-native";
import { NavigationContainer, DefaultTheme, DarkTheme } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Ionicons } from "@expo/vector-icons";
import SubscribedFeed from "@/screens/SubscribedFeed";
import AllFeed from "@/screens/AllFeed";
import Profile from "@/screens/Profile";
import {enableScreens} from "react-native-screens";
import {SafeAreaProvider} from "react-native-safe-area-context";
// Must be called before any navigator renders
enableScreens();
const Tab = createBottomTabNavigator();
const LIGHT_TAB = {
bg: "#FFFFFF",
border: "#E8E2D5",
active: "#C4622D",
inactive: "#8A8278",
label: "#1A1714",
};
const DARK_TAB = {
bg: "#161513",
border: "#2C2A27",
active: "#E07B45",
inactive: "#706D67",
label: "#F0EDE8",
};
export default function App() {
const scheme = useColorScheme();
const dark = scheme === "dark";
const tab = dark ? DARK_TAB : LIGHT_TAB;
const navTheme = dark
? { ...DarkTheme, colors: { ...DarkTheme.colors, background: "#0E0D0C" } }
: { ...DefaultTheme, colors: { ...DefaultTheme.colors, background: "#FDFAF5" } };
return (
<SafeAreaProvider>
<NavigationContainer theme={navTheme}>
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarStyle: {
backgroundColor: tab.bg,
borderTopColor: tab.border,
borderTopWidth: 1,
height: 60,
paddingBottom: 8,
paddingTop: 6,
},
tabBarActiveTintColor: tab.active,
tabBarInactiveTintColor: tab.inactive,
tabBarLabelStyle: {
fontSize: 10,
fontWeight: "600",
letterSpacing: 0.3,
},
tabBarIcon: ({ focused, color, size }) => {
const icons: Record<
string,
{ active: keyof typeof Ionicons.glyphMap; inactive: keyof typeof Ionicons.glyphMap }
> = {
"Subscribed": { active: "bookmark", inactive: "bookmark-outline" },
"Discover": { active: "compass", inactive: "compass-outline" },
"Profile": { active: "person-circle", inactive: "person-circle-outline" },
};
const set = icons[route.name];
return (
<Ionicons
name={focused ? set.active : set.inactive}
size={focused ? size + 1 : size}
color={color}
/>
);
},
})}
>
<Tab.Screen
name="Subscribed"
component={SubscribedFeed}
options={{ title: "My Feed" }}
/>
<Tab.Screen
name="Discover"
component={AllFeed}
options={{ title: "Discover" }}
/>
<Tab.Screen
name="Profile"
component={Profile}
options={{ title: "Profile" }}
/>
</Tab.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}

View File

@ -3,4 +3,4 @@ const { withNativeWind } = require('nativewind/metro');
const config = getDefaultConfig(__dirname) const config = getDefaultConfig(__dirname)
module.exports = withNativeWind(config, { input: './app/globals.css' }) module.exports = withNativeWind(config, { input: './globals.css' })

2132
package-lock.json generated

File diff suppressed because it is too large Load Diff

107
screens/AllFeed.tsx Normal file
View File

@ -0,0 +1,107 @@
import React, { useState, useMemo } from "react";
import {
View,
Text,
ScrollView,
useColorScheme,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import SearchBar from "../components/SearchBar";
import CollapsibleCategory, { Category } from "../components/CollapsibleCategory";
import { ALL_CATEGORIES } from "@/data/mockData";
export default function AllFeed() {
const [query, setQuery] = useState("");
const scheme = useColorScheme();
const dark = scheme === "dark";
const totalCount = ALL_CATEGORIES.reduce(
(sum, cat) => sum + cat.items.length,
0
);
// Filter items within each category based on search query
const filteredCategories = useMemo((): Category[] => {
if (!query.trim()) return ALL_CATEGORIES;
const lower = query.toLowerCase();
return ALL_CATEGORIES.map((cat) => ({
...cat,
items: cat.items.filter(
(item) =>
item.title.toLowerCase().includes(lower) ||
item.author.toLowerCase().includes(lower) ||
item.tag.toLowerCase().includes(lower) ||
cat.label.toLowerCase().includes(lower)
),
})).filter((cat) => cat.items.length > 0);
}, [query]);
return (
<SafeAreaView
className={`flex-1 ${dark ? "bg-obsidian-200" : "bg-parchment-50"}`}
>
<View
className={`border-b ${
dark ? "border-border-dark" : "border-border-light"
}`}
>
<View className="px-4 pt-2 pb-1">
<Text
className={`text-2xl font-display font-bold ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
>
Discover
</Text>
<Text
className={`text-xs mt-0.5 font-sans ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
{ALL_CATEGORIES.length} categories · {totalCount} articles
</Text>
</View>
<SearchBar
placeholder="Search all Subjects and Titles…"
onSearch={setQuery}
/>
</View>
<ScrollView
contentContainerStyle={{ paddingTop: 16, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
keyboardDismissMode="on-drag"
>
{filteredCategories.length === 0 ? (
<View className="items-center mt-16 px-8">
<Text className="text-4xl mb-3">🔍</Text>
<Text
className={`text-base font-sans font-semibold text-center ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
>
No results for &#34;{query}&#34;
</Text>
<Text
className={`text-sm font-sans text-center mt-1 ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
Try searching by category name, author, or topic.
</Text>
</View>
) : (
filteredCategories.map((cat, index) => (
<CollapsibleCategory
key={cat.id}
category={cat}
defaultOpen={index === 0} // first category open by default TODO maybe set option to change what is default
/>
))
)}
</ScrollView>
</SafeAreaView>
);
}

259
screens/Profile.tsx Normal file
View File

@ -0,0 +1,259 @@
import React from "react";
import {
View,
Text,
ScrollView,
TouchableOpacity,
useColorScheme,
Switch,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Ionicons } from "@expo/vector-icons";
interface SettingRowProps {
icon: keyof typeof Ionicons.glyphMap;
label: string;
value?: string;
toggle?: boolean;
toggleValue?: boolean;
onToggle?: (v: boolean) => void;
onPress?: () => void;
accent?: string;
}
{/*// TODO add logic idk im still missing a home/login screen*/}
function SettingRow({
icon,
label,
value,
toggle,
toggleValue,
onToggle,
onPress,
accent,
}: SettingRowProps) {
const scheme = useColorScheme();
const dark = scheme === "dark";
const iconColor = accent ?? (dark ? "#706D67" : "#8A8278");
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={onPress ? 0.7 : 1}
className={`flex-row items-center px-4 py-3.5 border-b ${
dark ? "border-border-dark" : "border-border-light"
}`}
>
<View
className="w-8 h-8 rounded-xl items-center justify-center mr-3"
style={{ backgroundColor: iconColor + "18" }}
>
<Ionicons name={icon} size={16} color={iconColor} />
</View>
<Text
className={`flex-1 text-sm font-sans ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
>
{label}
</Text>
{toggle ? (
<Switch
value={toggleValue}
onValueChange={onToggle}
trackColor={{
true: dark ? "#E07B45" : "#C4622D",
false: dark ? "#2C2A27" : "#E8E2D5",
}}
thumbColor="#FFFFFF"
/>
) : value ? (
<Text
className={`text-sm font-sans ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
{value}
</Text>
) : (
<Ionicons
name="chevron-forward"
size={14}
color={dark ? "#706D67" : "#8A8278"}
/>
)}
</TouchableOpacity>
);
}
function SectionLabel({ title }: { title: string }) {
const dark = useColorScheme() === "dark";
return (
<Text
className={`px-4 pt-5 pb-2 text-xs font-sans font-bold tracking-widest uppercase ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
{title}
</Text>
);
}
export default function Profile() {
const [notifications, setNotifications] = React.useState(true);
const [digest, setDigest] = React.useState(false);
const scheme = useColorScheme();
const dark = scheme === "dark";
return (
<SafeAreaView
className={`flex-1 ${dark ? "bg-obsidian-200" : "bg-parchment-50"}`}
>
<ScrollView
contentContainerStyle={{ paddingBottom: 48 }}
showsVerticalScrollIndicator={false}
>
{/* Avatar + name block */}
<View
className={`items-center pt-8 pb-6 mx-4 mt-4 rounded-3xl ${
dark ? "bg-obsidian-100" : "bg-surface-light"
}`}
style={{
shadowColor: dark ? "#000" : "#1A1714",
shadowOpacity: dark ? 0.35 : 0.06,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 10,
elevation: 3,
}}
>
{/*// TODO*/}
{/* Avatar placeholder */}
<View
className={`w-20 h-20 rounded-full items-center justify-center mb-3 ${
dark ? "bg-accent-dark/20" : "bg-accent/10"
}`}
>
<Text className="text-3xl">📰</Text>
</View>
<Text
className={`text-xl font-display font-bold ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
>
Get Name here idk
</Text>
<Text
className={`text-sm font-sans mt-1 ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
{/*// TODO*/}
aaa@aaaq.com
</Text>
{/* Stats row */}
<View className="flex-row mt-5 gap-8">
{[
//TODO add stats tracked
{ label: "Subscribed", value: "5" },
{ label: "Read", value: "128" },
{ label: "Saved", value: "34" },
].map((stat) => (
<View key={stat.label} className="items-center">
<Text
className={`text-xl font-display font-bold ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
>
{stat.value}
</Text>
<Text
className={`text-xs font-sans mt-0.5 ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
{stat.label}
</Text>
</View>
))}
</View>
</View>
{/* Settings sections */}
<View
className={`mx-4 mt-5 rounded-2xl overflow-hidden border ${
dark
? "bg-obsidian-100 border-border-dark"
: "bg-surface-light border-border-light"
}`}
>
<SectionLabel title="Notifications" />
<SettingRow
icon="notifications-outline"
label="Push notifications"
toggle
toggleValue={notifications}
onToggle={setNotifications}
accent={dark ? "#E07B45" : "#C4622D"}
/>
<SettingRow
icon="mail-outline"
label="Daily digest email"
toggle
toggleValue={digest}
onToggle={setDigest}
accent={dark ? "#5E9EF4" : "#3B7DD8"}
/>
<SectionLabel title="Subscriptions" />
<SettingRow
icon="bookmark-outline"
label="Manage subscriptions"
onPress={() => {}}
accent={dark ? "#4EC992" : "#2E9E6B"}
/>
<SectionLabel title="Account" />
<SettingRow
icon="person-outline"
label="Edit profile"
onPress={() => {}}
/>
{/*
<SettingRow
icon="lock-closed-outline"
label="Privacy settings"
onPress={() => {}}
/>
*/}
<SettingRow
icon="help-circle-outline"
label="Help & support"
onPress={() => {}}
/>
</View>
{/* Sign out */}
<TouchableOpacity
className={`mx-4 mt-4 py-4 rounded-2xl items-center border ${
dark
? "border-red-900/40 bg-red-950/20"
: "border-red-200 bg-red-50"
}`}
activeOpacity={0.7}
>
<Text className="text-sm font-sans font-semibold text-red-500">
Sign out
</Text>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
}

View File

@ -0,0 +1,92 @@
import React, { useState, useMemo } from "react";
import {
View,
Text,
ScrollView,
useColorScheme,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import SearchBar from "../components/SearchBar";
import ExpandableItem from "../components/ExpandableItem";
import { SUBSCRIBED_ITEMS } from "@/data/mockData";
export default function SubscribedFeed() {
const [query, setQuery] = useState("");
const scheme = useColorScheme();
const dark = scheme === "dark";
const filtered = useMemo(() => {
if (!query.trim()) return SUBSCRIBED_ITEMS;
const lower = query.toLowerCase();
return SUBSCRIBED_ITEMS.filter(
(item) =>
item.title.toLowerCase().includes(lower) ||
item.author.toLowerCase().includes(lower) ||
item.tag.toLowerCase().includes(lower)
);
}, [query]);
return (
<SafeAreaView
className={`flex-1 ${dark ? "bg-obsidian-200" : "bg-parchment-50"}`}
>
<View
className={`border-b ${
dark ? "border-border-dark" : "border-border-light"
}`}
>
<View className="px-4 pt-2 pb-1">
<Text
className={`text-2xl font-display font-bold ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
>
Your Feed
</Text>
<Text
className={`text-xs mt-0.5 font-sans ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
{SUBSCRIBED_ITEMS.length} subscriptions · updated just now
</Text>
</View>
<SearchBar
placeholder="Search your subscribed subjects…"
onSearch={setQuery}
/>
</View>
{/* Scrollable list */}
<ScrollView
contentContainerStyle={{ paddingTop: 12, paddingBottom: 32 }}
showsVerticalScrollIndicator={false}
keyboardDismissMode="on-drag"
>
{filtered.length === 0 ? (
<View className="items-center mt-16 px-8">
<Text className="text-4xl mb-3">🔍</Text>
<Text
className={`text-base font-sans font-semibold text-center ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
>
No results for &#34;{query}&#34;
</Text>
<Text
className={`text-sm font-sans text-center mt-1 ${
dark ? "text-muted-dark" : "text-muted-light"
}`}
>
Try a different title, author, or topic tag.
</Text>
</View>
) : (
filtered.map((item) => <ExpandableItem key={item.id} item={item} />)
)}
</ScrollView>
</SafeAreaView>
);
}

View File

@ -1,15 +1,84 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
// NOTE: Update this to include the paths to all files that contain Nativewind classes. // NOTE: Update this to include the paths to all files that contain Nativewind classes.
content: ["./app/**/*.{js,jsx,ts,tsx}"], content: [
"./app/**/*.{js,jsx,ts,tsx}",
"./screens/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
],
presets: [require("nativewind/preset")], presets: [require("nativewind/preset")],
darkMode: "media", // follows system preference
theme: { theme: {
extend: { extend: {
colors: { colors: {
primary: '#43ff64d9', // ── Brand accent ──────────────────────────────
secondary: '#3885BA', accent: {
} DEFAULT: "#C4622D",
dark: "#E07B45",
muted: "#E8A882",
},
// ── Light theme surfaces ───────────────────────
parchment: {
50: "#FDFAF5",
100: "#F7F3EB",
200: "#EDE7D9",
300: "#DDD5C4",
400: "#C9BEA9",
},
// ── Dark theme surfaces ────────────────────────
obsidian: {
50: "#2A2825",
100: "#1F1E1B",
200: "#161513",
300: "#0E0D0C",
400: "#080807",
},
// ── Semantic aliases (used in components) ──────
surface: {
light: "#FFFFFF",
dark: "#1C1B19",
},
border: {
light: "#E8E2D5",
dark: "#2C2A27",
},
ink: {
light: "#1A1714",
dark: "#F0EDE8",
},
muted: {
light: "#8A8278",
dark: "#706D67",
},
},
fontFamily: {
// Use with expo-font or react-native-google-fonts
display: ["Playfair Display", "serif"],
sans: ["DM Sans", "sans-serif"],
mono: ["DM Mono", "monospace"],
},
borderRadius: {
"4xl": "2rem",
},
spacing: {
"18": "4.5rem",
"22": "5.5rem",
},
boxShadow: {
card: "0 1px 4px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.06)",
"card-dark": "0 1px 4px rgba(0,0,0,0.4), 0 4px 16px rgba(0,0,0,0.3)",
tab: "0 -1px 0 rgba(0,0,0,0.06)",
},
}, },
}, },
plugins: [], plugins: [],
} };