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:
parent
97aea8e940
commit
ac219aab0b
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
12
.idea/etfoglasi-frontend.iml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
59
app/(tabs)/_layout.tsx
Normal file
59
app/(tabs)/_layout.tsx
Normal 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
2
app/(tabs)/all.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
import AllFeed from "@/screens/AllFeed";
|
||||
export default AllFeed;
|
||||
3
app/(tabs)/index.tsx
Normal file
3
app/(tabs)/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import SubscribedFeed from "@/screens/SubscribedFeed";
|
||||
export default SubscribedFeed;
|
||||
|
||||
2
app/(tabs)/profile.tsx
Normal file
2
app/(tabs)/profile.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
import Profile from "@/screens/Profile";
|
||||
export default Profile;
|
||||
@ -1,6 +1,6 @@
|
||||
import "../globals.css";
|
||||
import { Stack } from "expo-router";
|
||||
import './globals.css'
|
||||
|
||||
export default function RootLayout() {
|
||||
return <Stack />;
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
111
components/CollapsibleCategory.tsx
Normal file
111
components/CollapsibleCategory.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
153
components/ExpandableItem.tsx
Normal file
153
components/ExpandableItem.tsx
Normal 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
66
components/SearchBar.tsx
Normal 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
196
data/mockData.ts
Normal 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 Newton’s 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ödinger’s 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
114
index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -3,4 +3,4 @@ const { withNativeWind } = require('nativewind/metro');
|
||||
|
||||
const config = getDefaultConfig(__dirname)
|
||||
|
||||
module.exports = withNativeWind(config, { input: './app/globals.css' })
|
||||
module.exports = withNativeWind(config, { input: './globals.css' })
|
||||
|
||||
2132
package-lock.json
generated
2132
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
107
screens/AllFeed.tsx
Normal file
107
screens/AllFeed.tsx
Normal 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 "{query}"
|
||||
</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
259
screens/Profile.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
92
screens/SubscribedFeed.tsx
Normal file
92
screens/SubscribedFeed.tsx
Normal 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 "{query}"
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -1,15 +1,84 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
// 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")],
|
||||
|
||||
darkMode: "media", // follows system preference
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#43ff64d9',
|
||||
secondary: '#3885BA',
|
||||
}
|
||||
// ── Brand accent ──────────────────────────────
|
||||
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: [],
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user