mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add ai and todo example templates for native frontends (#293)
This commit is contained in:
5
.changeset/grumpy-bees-appear.md
Normal file
5
.changeset/grumpy-bees-appear.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add AI and todo example templates for nativewind and unistyles
|
||||||
@@ -145,9 +145,17 @@ function getNativeInstructions(isConvex: boolean): string {
|
|||||||
? "your Convex deployment URL (find after running 'dev:setup')"
|
? "your Convex deployment URL (find after running 'dev:setup')"
|
||||||
: "your local IP address";
|
: "your local IP address";
|
||||||
|
|
||||||
return `${pc.yellow(
|
let instructions = `${pc.yellow(
|
||||||
"NOTE:",
|
"NOTE:",
|
||||||
)} For Expo connectivity issues, update apps/native/${envFileName} \nwith ${ipNote}:\n${`${envVar}=${exampleUrl}`}\n`;
|
)} For Expo connectivity issues, update apps/native/${envFileName} \nwith ${ipNote}:\n${`${envVar}=${exampleUrl}`}\n`;
|
||||||
|
|
||||||
|
if (isConvex) {
|
||||||
|
instructions += `\n${pc.yellow(
|
||||||
|
"IMPORTANT:",
|
||||||
|
)} When using local development with Convex and native apps, ensure you use your local IP address \ninstead of localhost or 127.0.0.1 for proper connectivity.\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instructions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLintingInstructions(runCmd?: string): string {
|
function getLintingInstructions(runCmd?: string): string {
|
||||||
|
|||||||
@@ -599,6 +599,8 @@ export async function setupExamplesTemplate(
|
|||||||
|
|
||||||
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
||||||
const webAppDirExists = await fs.pathExists(webAppDir);
|
const webAppDirExists = await fs.pathExists(webAppDir);
|
||||||
|
const nativeAppDir = path.join(projectDir, "apps/native");
|
||||||
|
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
|
||||||
|
|
||||||
const hasReactWeb = context.frontend.some((f) =>
|
const hasReactWeb = context.frontend.some((f) =>
|
||||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||||
@@ -758,6 +760,34 @@ export async function setupExamplesTemplate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nativeAppDirExists) {
|
||||||
|
const hasNativeWind = context.frontend.includes("native-nativewind");
|
||||||
|
const hasUnistyles = context.frontend.includes("native-unistyles");
|
||||||
|
|
||||||
|
if (hasNativeWind || hasUnistyles) {
|
||||||
|
let nativeFramework = "";
|
||||||
|
if (hasNativeWind) {
|
||||||
|
nativeFramework = "nativewind";
|
||||||
|
} else if (hasUnistyles) {
|
||||||
|
nativeFramework = "unistyles";
|
||||||
|
}
|
||||||
|
|
||||||
|
const exampleNativeSrc = path.join(
|
||||||
|
exampleBaseDir,
|
||||||
|
`native/${nativeFramework}`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(exampleNativeSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
exampleNativeSrc,
|
||||||
|
nativeAppDir,
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export async function setupExamples(config: ProjectConfig): Promise<void> {
|
|||||||
frontend.includes("react-router") ||
|
frontend.includes("react-router") ||
|
||||||
frontend.includes("tanstack-router") ||
|
frontend.includes("tanstack-router") ||
|
||||||
frontend.includes("next") ||
|
frontend.includes("next") ||
|
||||||
frontend.includes("tanstack-start");
|
frontend.includes("tanstack-start") ||
|
||||||
|
frontend.includes("native-nativewind") ||
|
||||||
|
frontend.includes("native-unistyles");
|
||||||
|
|
||||||
if (clientDirExists) {
|
if (clientDirExists) {
|
||||||
const dependencies: AvailableDependencies[] = ["ai"];
|
const dependencies: AvailableDependencies[] = ["ai"];
|
||||||
|
|||||||
@@ -25,30 +25,9 @@ export async function getExamplesChoice(
|
|||||||
|
|
||||||
if (database === "none") return [];
|
if (database === "none") return [];
|
||||||
|
|
||||||
const onlyNative =
|
|
||||||
frontends &&
|
|
||||||
frontends.length === 1 &&
|
|
||||||
(frontends[0] === "native-nativewind" ||
|
|
||||||
frontends[0] === "native-unistyles");
|
|
||||||
if (onlyNative) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasWebFrontend =
|
|
||||||
frontends?.some((f) =>
|
|
||||||
[
|
|
||||||
"react-router",
|
|
||||||
"tanstack-router",
|
|
||||||
"tanstack-start",
|
|
||||||
"next",
|
|
||||||
"nuxt",
|
|
||||||
"svelte",
|
|
||||||
"solid",
|
|
||||||
].includes(f),
|
|
||||||
) ?? false;
|
|
||||||
const noFrontendSelected = !frontends || frontends.length === 0;
|
const noFrontendSelected = !frontends || frontends.length === 0;
|
||||||
|
|
||||||
if (!hasWebFrontend && !noFrontendSelected) return [];
|
if (noFrontendSelected) return [];
|
||||||
|
|
||||||
let response: Examples[] | symbol = [];
|
let response: Examples[] | symbol = [];
|
||||||
const options: { value: Examples; label: string; hint: string }[] = [
|
const options: { value: Examples; label: string; hint: string }[] = [
|
||||||
|
|||||||
@@ -467,24 +467,6 @@ export function validateConfigCompatibility(
|
|||||||
config.addons = [...new Set(config.addons)];
|
config.addons = [...new Set(config.addons)];
|
||||||
}
|
}
|
||||||
|
|
||||||
const onlyNativeFrontend =
|
|
||||||
effectiveFrontend &&
|
|
||||||
effectiveFrontend.length === 1 &&
|
|
||||||
(effectiveFrontend[0] === "native-nativewind" ||
|
|
||||||
effectiveFrontend[0] === "native-unistyles");
|
|
||||||
|
|
||||||
if (
|
|
||||||
onlyNativeFrontend &&
|
|
||||||
config.examples &&
|
|
||||||
config.examples.length > 0 &&
|
|
||||||
!config.examples.includes("none")
|
|
||||||
) {
|
|
||||||
consola.fatal(
|
|
||||||
"Examples are not supported when only a native frontend (NativeWind or Unistyles) is selected.",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
config.examples &&
|
config.examples &&
|
||||||
config.examples.length > 0 &&
|
config.examples.length > 0 &&
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { useRef, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
ScrollView,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
} from "react-native";
|
||||||
|
import { useChat } from "@ai-sdk/react";
|
||||||
|
import { fetch as expoFetch } from "expo/fetch";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { Container } from "@/components/container";
|
||||||
|
|
||||||
|
// Utility function to generate API URLs
|
||||||
|
const generateAPIUrl = (relativePath: string) => {
|
||||||
|
const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL;
|
||||||
|
if (!serverUrl) {
|
||||||
|
throw new Error("EXPO_PUBLIC_SERVER_URL environment variable is not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
|
||||||
|
return serverUrl.concat(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AIScreen() {
|
||||||
|
const { messages, input, handleInputChange, handleSubmit, error } = useChat({
|
||||||
|
fetch: expoFetch as unknown as typeof globalThis.fetch,
|
||||||
|
api: generateAPIUrl('/ai'),
|
||||||
|
onError: error => console.error(error, 'AI Chat Error'),
|
||||||
|
maxSteps: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (input.trim()) {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<View className="flex-1 justify-center items-center px-4">
|
||||||
|
<Text className="text-destructive text-center text-lg mb-4">
|
||||||
|
Error: {error.message}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-muted-foreground text-center">
|
||||||
|
Please check your connection and try again.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
className="flex-1"
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
>
|
||||||
|
<View className="flex-1 px-4 py-6">
|
||||||
|
<View className="mb-6">
|
||||||
|
<Text className="text-foreground text-2xl font-bold mb-2">
|
||||||
|
AI Chat
|
||||||
|
</Text>
|
||||||
|
<Text className="text-muted-foreground">
|
||||||
|
Chat with our AI assistant
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
className="flex-1 mb-4"
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<View className="flex-1 justify-center items-center">
|
||||||
|
<Text className="text-center text-muted-foreground text-lg">
|
||||||
|
Ask me anything to get started!
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className="space-y-4">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<View
|
||||||
|
key={message.id}
|
||||||
|
className={`p-3 rounded-lg ${
|
||||||
|
message.role === "user"
|
||||||
|
? "bg-primary/10 ml-8"
|
||||||
|
: "bg-card mr-8 border border-border"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Text className="text-sm font-semibold mb-1 text-foreground">
|
||||||
|
{message.role === "user" ? "You" : "AI Assistant"}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-foreground leading-relaxed">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View className="border-t border-border pt-4">
|
||||||
|
<View className="flex-row items-end space-x-2">
|
||||||
|
<TextInput
|
||||||
|
value={input}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange({
|
||||||
|
...e,
|
||||||
|
target: {
|
||||||
|
...e.target,
|
||||||
|
value: e.nativeEvent.text,
|
||||||
|
},
|
||||||
|
} as unknown as React.ChangeEvent<HTMLInputElement>)
|
||||||
|
}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
placeholderTextColor="#6b7280"
|
||||||
|
className="flex-1 border border-border rounded-md px-3 py-2 text-foreground bg-background min-h-[40px] max-h-[120px]"
|
||||||
|
onSubmitEditing={(e) => {
|
||||||
|
handleSubmit(e);
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onSubmit}
|
||||||
|
disabled={!input.trim()}
|
||||||
|
className={`p-2 rounded-md ${
|
||||||
|
input.trim()
|
||||||
|
? "bg-primary"
|
||||||
|
: "bg-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="send"
|
||||||
|
size={20}
|
||||||
|
color={input.trim() ? "#ffffff" : "#6b7280"}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import structuredClone from "@ungap/structured-clone";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
|
if (Platform.OS !== "web") {
|
||||||
|
const setupPolyfills = async () => {
|
||||||
|
const { polyfillGlobal } = await import(
|
||||||
|
"react-native/Libraries/Utilities/PolyfillFunctions"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { TextEncoderStream, TextDecoderStream } = await import(
|
||||||
|
"@stardazed/streams-text-encoding"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!("structuredClone" in global)) {
|
||||||
|
polyfillGlobal("structuredClone", () => structuredClone);
|
||||||
|
}
|
||||||
|
|
||||||
|
polyfillGlobal("TextEncoderStream", () => TextEncoderStream);
|
||||||
|
polyfillGlobal("TextDecoderStream", () => TextDecoderStream);
|
||||||
|
};
|
||||||
|
|
||||||
|
setupPolyfills();
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import { useRef, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
ScrollView,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
} from "react-native";
|
||||||
|
import { useChat } from "@ai-sdk/react";
|
||||||
|
import { fetch as expoFetch } from "expo/fetch";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { Container } from "@/components/container";
|
||||||
|
|
||||||
|
const generateAPIUrl = (relativePath: string) => {
|
||||||
|
const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL;
|
||||||
|
if (!serverUrl) {
|
||||||
|
throw new Error(
|
||||||
|
"EXPO_PUBLIC_SERVER_URL environment variable is not defined",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = relativePath.startsWith("/") ? relativePath : `/${relativePath}`;
|
||||||
|
return serverUrl.concat(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AIScreen() {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
const { messages, input, handleInputChange, handleSubmit, error } = useChat({
|
||||||
|
fetch: expoFetch as unknown as typeof globalThis.fetch,
|
||||||
|
api: generateAPIUrl("/ai"),
|
||||||
|
onError: (error) => console.error(error, "AI Chat Error"),
|
||||||
|
maxSteps: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (input.trim()) {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Text style={styles.errorText}>Error: {error.message}</Text>
|
||||||
|
<Text style={styles.errorSubtext}>
|
||||||
|
Please check your connection and try again.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.container}
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.headerTitle}>AI Chat</Text>
|
||||||
|
<Text style={styles.headerSubtitle}>
|
||||||
|
Chat with our AI assistant
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
style={styles.messagesContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<View style={styles.emptyContainer}>
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
Ask me anything to get started!
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.messagesWrapper}>
|
||||||
|
{messages.map((message) => (
|
||||||
|
<View
|
||||||
|
key={message.id}
|
||||||
|
style={[
|
||||||
|
styles.messageContainer,
|
||||||
|
message.role === "user"
|
||||||
|
? styles.userMessage
|
||||||
|
: styles.assistantMessage,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={styles.messageRole}>
|
||||||
|
{message.role === "user" ? "You" : "AI Assistant"}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.messageContent}>{message.content}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View style={styles.inputSection}>
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<TextInput
|
||||||
|
value={input}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange({
|
||||||
|
...e,
|
||||||
|
target: {
|
||||||
|
...e.target,
|
||||||
|
value: e.nativeEvent.text,
|
||||||
|
},
|
||||||
|
} as unknown as React.ChangeEvent<HTMLInputElement>)
|
||||||
|
}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
placeholderTextColor={theme.colors.border}
|
||||||
|
style={styles.textInput}
|
||||||
|
onSubmitEditing={(e) => {
|
||||||
|
handleSubmit(e);
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onSubmit}
|
||||||
|
disabled={!input.trim()}
|
||||||
|
style={[
|
||||||
|
styles.sendButton,
|
||||||
|
!input.trim() && styles.sendButtonDisabled,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="send"
|
||||||
|
size={20}
|
||||||
|
color={
|
||||||
|
input.trim() ? theme.colors.background : theme.colors.border
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: theme.spacing.md,
|
||||||
|
paddingVertical: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
errorContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: theme.spacing.md,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: theme.colors.destructive,
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 18,
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
|
},
|
||||||
|
errorSubtext: {
|
||||||
|
color: theme.colors.typography,
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
marginBottom: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.colors.typography,
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
headerSubtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.typography,
|
||||||
|
},
|
||||||
|
messagesContainer: {
|
||||||
|
flex: 1,
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
|
},
|
||||||
|
emptyContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
textAlign: "center",
|
||||||
|
color: theme.colors.typography,
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
messagesWrapper: {
|
||||||
|
gap: theme.spacing.md,
|
||||||
|
},
|
||||||
|
messageContainer: {
|
||||||
|
padding: theme.spacing.md,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
userMessage: {
|
||||||
|
backgroundColor: theme.colors.primary + "20",
|
||||||
|
marginLeft: theme.spacing.xl,
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
},
|
||||||
|
assistantMessage: {
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
marginRight: theme.spacing.xl,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
messageRole: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
|
color: theme.colors.typography,
|
||||||
|
},
|
||||||
|
messageContent: {
|
||||||
|
color: theme.colors.typography,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
toolInvocations: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: theme.colors.typography,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
backgroundColor: theme.colors.border + "40",
|
||||||
|
padding: theme.spacing.sm,
|
||||||
|
borderRadius: 4,
|
||||||
|
marginTop: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
inputSection: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: theme.colors.border,
|
||||||
|
paddingTop: theme.spacing.md,
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: theme.spacing.md,
|
||||||
|
paddingVertical: theme.spacing.sm,
|
||||||
|
color: theme.colors.typography,
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
fontSize: 16,
|
||||||
|
minHeight: 40,
|
||||||
|
maxHeight: 120,
|
||||||
|
},
|
||||||
|
sendButton: {
|
||||||
|
backgroundColor: theme.colors.primary,
|
||||||
|
padding: theme.spacing.sm,
|
||||||
|
borderRadius: 8,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
sendButtonDisabled: {
|
||||||
|
backgroundColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
}));
|
||||||
25
apps/cli/templates/examples/ai/native/unistyles/polyfills.js
Normal file
25
apps/cli/templates/examples/ai/native/unistyles/polyfills.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import structuredClone from "@ungap/structured-clone";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
|
if (Platform.OS !== "web") {
|
||||||
|
const setupPolyfills = async () => {
|
||||||
|
const { polyfillGlobal } = await import(
|
||||||
|
"react-native/Libraries/Utilities/PolyfillFunctions"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { TextEncoderStream, TextDecoderStream } = await import(
|
||||||
|
"@stardazed/streams-text-encoding"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!("structuredClone" in global)) {
|
||||||
|
polyfillGlobal("structuredClone", () => structuredClone);
|
||||||
|
}
|
||||||
|
|
||||||
|
polyfillGlobal("TextEncoderStream", () => TextEncoderStream);
|
||||||
|
polyfillGlobal("TextDecoderStream", () => TextDecoderStream);
|
||||||
|
};
|
||||||
|
|
||||||
|
setupPolyfills();
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
ScrollView,
|
||||||
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
|
||||||
|
{{#if (eq backend "convex")}}
|
||||||
|
import { useMutation, useQuery } from "convex/react";
|
||||||
|
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
||||||
|
import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
|
||||||
|
{{else}}
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
import { Container } from "@/components/container";
|
||||||
|
{{#unless (eq backend "convex")}}
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
import { orpc } from "@/utils/orpc";
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
|
import { trpc } from "@/utils/trpc";
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
export default function TodosScreen() {
|
||||||
|
const [newTodoText, setNewTodoText] = useState("");
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
|
{{#if (eq backend "convex")}}
|
||||||
|
const todos = useQuery(api.todos.getAll);
|
||||||
|
const createTodoMutation = useMutation(api.todos.create);
|
||||||
|
const toggleTodoMutation = useMutation(api.todos.toggle);
|
||||||
|
const deleteTodoMutation = useMutation(api.todos.deleteTodo);
|
||||||
|
|
||||||
|
const handleAddTodo = async () => {
|
||||||
|
const text = newTodoText.trim();
|
||||||
|
if (!text) return;
|
||||||
|
await createTodoMutation({ text });
|
||||||
|
setNewTodoText("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
|
||||||
|
toggleTodoMutation({ id, completed: !currentCompleted });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTodo = (id: Id<"todos">) => {
|
||||||
|
Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [
|
||||||
|
{ text: "Cancel", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Delete",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: () => deleteTodoMutation({ id }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
{{else}}
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
const todos = useQuery(orpc.todo.getAll.queryOptions());
|
||||||
|
const createMutation = useMutation(
|
||||||
|
orpc.todo.create.mutationOptions({
|
||||||
|
onSuccess: () => {
|
||||||
|
todos.refetch();
|
||||||
|
setNewTodoText("");
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const toggleMutation = useMutation(
|
||||||
|
orpc.todo.toggle.mutationOptions({
|
||||||
|
onSuccess: () => todos.refetch(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const deleteMutation = useMutation(
|
||||||
|
orpc.todo.delete.mutationOptions({
|
||||||
|
onSuccess: () => todos.refetch(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
|
const todos = useQuery(trpc.todo.getAll.queryOptions());
|
||||||
|
const createMutation = useMutation(
|
||||||
|
trpc.todo.create.mutationOptions({
|
||||||
|
onSuccess: () => {
|
||||||
|
todos.refetch();
|
||||||
|
setNewTodoText("");
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const toggleMutation = useMutation(
|
||||||
|
trpc.todo.toggle.mutationOptions({
|
||||||
|
onSuccess: () => todos.refetch(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const deleteMutation = useMutation(
|
||||||
|
trpc.todo.delete.mutationOptions({
|
||||||
|
onSuccess: () => todos.refetch(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
const handleAddTodo = () => {
|
||||||
|
if (newTodoText.trim()) {
|
||||||
|
createMutation.mutate({ text: newTodoText });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleTodo = (id: number, completed: boolean) => {
|
||||||
|
toggleMutation.mutate({ id, completed: !completed });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTodo = (id: number) => {
|
||||||
|
Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [
|
||||||
|
{ text: "Cancel", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Delete",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: () => deleteMutation.mutate({ id }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
const isLoading = {{#if (eq backend "convex")}}!todos{{else}}todos.isLoading{{/if}};
|
||||||
|
const isCreating = {{#if (eq backend "convex")}}false{{else}}createMutation.isPending{{/if}};
|
||||||
|
const primaryButtonTextColor = theme.colors.background;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<ScrollView style={styles.scrollView}>
|
||||||
|
<View style={styles.headerContainer}>
|
||||||
|
<Text style={styles.headerTitle}>Todo List</Text>
|
||||||
|
<Text style={styles.headerSubtitle}>
|
||||||
|
Manage your tasks efficiently
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<TextInput
|
||||||
|
value={newTodoText}
|
||||||
|
onChangeText={setNewTodoText}
|
||||||
|
placeholder="Add a new task..."
|
||||||
|
placeholderTextColor={theme.colors.border}
|
||||||
|
editable={!isCreating}
|
||||||
|
style={styles.textInput}
|
||||||
|
onSubmitEditing={handleAddTodo}
|
||||||
|
returnKeyType="done"
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleAddTodo}
|
||||||
|
disabled={isCreating || !newTodoText.trim()}
|
||||||
|
style={[
|
||||||
|
styles.addButton,
|
||||||
|
(isCreating || !newTodoText.trim()) && styles.addButtonDisabled,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{isCreating ? (
|
||||||
|
<ActivityIndicator size="small" color={primaryButtonTextColor} />
|
||||||
|
) : (
|
||||||
|
<Ionicons
|
||||||
|
name="add"
|
||||||
|
size={24}
|
||||||
|
color={primaryButtonTextColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color={theme.colors.primary} />
|
||||||
|
<Text style={styles.loadingText}>Loading todos...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{{#if (eq backend "convex")}}
|
||||||
|
{todos && todos.length === 0 && !isLoading && (
|
||||||
|
<Text style={styles.emptyText}>No todos yet. Add one!</Text>
|
||||||
|
)}
|
||||||
|
{todos?.map((todo) => (
|
||||||
|
<View key={todo._id} style={styles.todoItem}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleToggleTodo(todo._id, todo.completed)}
|
||||||
|
style={styles.todoContent}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={todo.completed ? "checkbox" : "square-outline"}
|
||||||
|
size={24}
|
||||||
|
color={todo.completed ? theme.colors.primary : theme.colors.typography}
|
||||||
|
style={styles.checkbox}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.todoText,
|
||||||
|
todo.completed && styles.todoTextCompleted,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{todo.text}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={() => handleDeleteTodo(todo._id)}>
|
||||||
|
<Ionicons name="trash-outline" size={24} color={theme.colors.destructive} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{{else}}
|
||||||
|
{todos.data && todos.data.length === 0 && !isLoading && (
|
||||||
|
<Text style={styles.emptyText}>No todos yet. Add one!</Text>
|
||||||
|
)}
|
||||||
|
{todos.data?.map((todo: { id: number; text: string; completed: boolean }) => (
|
||||||
|
<View key={todo.id} style={styles.todoItem}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleToggleTodo(todo.id, todo.completed)}
|
||||||
|
style={styles.todoContent}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={todo.completed ? "checkbox" : "square-outline"}
|
||||||
|
size={24}
|
||||||
|
color={todo.completed ? theme.colors.primary : theme.colors.typography}
|
||||||
|
style={styles.checkbox}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.todoText,
|
||||||
|
todo.completed && styles.todoTextCompleted,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{todo.text}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={() => handleDeleteTodo(todo.id)}>
|
||||||
|
<Ionicons name="trash-outline" size={24} color={theme.colors.destructive} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{{/if}}
|
||||||
|
</ScrollView>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
scrollView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
headerContainer: {
|
||||||
|
paddingHorizontal: theme.spacing.md,
|
||||||
|
paddingVertical: theme.spacing.lg,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: theme.colors.border,
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.colors.typography,
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
headerSubtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.typography,
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: theme.spacing.md,
|
||||||
|
paddingVertical: theme.spacing.sm,
|
||||||
|
color: theme.colors.typography,
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
marginRight: theme.spacing.sm,
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
addButton: {
|
||||||
|
backgroundColor: theme.colors.primary,
|
||||||
|
padding: theme.spacing.sm,
|
||||||
|
borderRadius: 8,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
addButtonDisabled: {
|
||||||
|
backgroundColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: theme.spacing.sm,
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.typography,
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: theme.spacing.xl,
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.typography,
|
||||||
|
},
|
||||||
|
todoItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: theme.spacing.md,
|
||||||
|
paddingHorizontal: theme.spacing.md,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: theme.colors.border,
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
},
|
||||||
|
todoContent: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
marginRight: theme.spacing.md,
|
||||||
|
},
|
||||||
|
todoText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.typography,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
todoTextCompleted: {
|
||||||
|
textDecorationLine: "line-through",
|
||||||
|
color: theme.colors.border,
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -1,27 +1,44 @@
|
|||||||
|
import { TabBarIcon } from "@/components/tabbar-icon";
|
||||||
|
import { useColorScheme } from "@/lib/use-color-scheme";
|
||||||
import { Tabs } from "expo-router";
|
import { Tabs } from "expo-router";
|
||||||
|
|
||||||
import { TabBarIcon } from "@/components/tabbar-icon";
|
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
|
const { isDarkColorScheme } = useColorScheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
tabBarActiveTintColor: "black",
|
tabBarActiveTintColor: isDarkColorScheme
|
||||||
|
? "hsl(217.2 91.2% 59.8%)"
|
||||||
|
: "hsl(221.2 83.2% 53.3%)",
|
||||||
|
tabBarInactiveTintColor: isDarkColorScheme
|
||||||
|
? "hsl(215 20.2% 65.1%)"
|
||||||
|
: "hsl(215.4 16.3% 46.9%)",
|
||||||
|
tabBarStyle: {
|
||||||
|
backgroundColor: isDarkColorScheme
|
||||||
|
? "hsl(222.2 84% 4.9%)"
|
||||||
|
: "hsl(0 0% 100%)",
|
||||||
|
borderTopColor: isDarkColorScheme
|
||||||
|
? "hsl(217.2 32.6% 17.5%)"
|
||||||
|
: "hsl(214.3 31.8% 91.4%)",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
title: "Tab One",
|
title: "Home",
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="two"
|
name="two"
|
||||||
options={{
|
options={{
|
||||||
title: "Tab Two",
|
title: "Explore",
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
tabBarIcon: ({ color }) => (
|
||||||
|
<TabBarIcon name="compass" color={color} />
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { Text, View } from "react-native";
|
import { ScrollView, Text, View } from "react-native";
|
||||||
|
|
||||||
export default function TabOne() {
|
export default function TabOne() {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<View className="p-6 flex-1 justify-center">
|
<ScrollView className="flex-1 p-6">
|
||||||
<Text className="text-2xl font-bold text-foreground text-center mb-4">
|
<View className="py-8">
|
||||||
Tab One
|
<Text className="text-3xl font-bold text-foreground mb-2">
|
||||||
</Text>
|
Tab One
|
||||||
<Text className="text-foreground text-center">
|
</Text>
|
||||||
This is the first tab of the application.
|
<Text className="text-lg text-muted-foreground">
|
||||||
</Text>
|
Explore the first section of your app
|
||||||
</View>
|
</Text>
|
||||||
</Container>
|
</View>
|
||||||
);
|
</ScrollView>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { Text, View } from "react-native";
|
import { ScrollView, Text, View } from "react-native";
|
||||||
|
|
||||||
export default function TabTwo() {
|
export default function TabTwo() {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<View className="p-6 flex-1 justify-center">
|
<ScrollView className="flex-1 p-6">
|
||||||
<Text className="text-2xl font-bold text-foreground text-center mb-4">
|
<View className="py-8">
|
||||||
Tab Two
|
<Text className="text-3xl font-bold text-foreground mb-2">
|
||||||
</Text>
|
Tab Two
|
||||||
<Text className="text-foreground text-center">
|
</Text>
|
||||||
This is the second tab of the application.
|
<Text className="text-lg text-muted-foreground">
|
||||||
</Text>
|
Discover more features and content
|
||||||
</View>
|
</Text>
|
||||||
</Container>
|
</View>
|
||||||
);
|
</ScrollView>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,6 @@ const DrawerLayout = () => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{{#if (includes examples "todo")}}
|
|
||||||
<Drawer.Screen
|
|
||||||
name="todos"
|
|
||||||
options=\{{
|
|
||||||
headerTitle: "Todos",
|
|
||||||
drawerLabel: "Todos",
|
|
||||||
drawerIcon: ({ size, color }) => (
|
|
||||||
<Ionicons name="checkbox-outline" size={size} color={color} />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
name="(tabs)"
|
name="(tabs)"
|
||||||
options=\{{
|
options=\{{
|
||||||
@@ -44,6 +32,34 @@ const DrawerLayout = () => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{{#if (includes examples "todo")}}
|
||||||
|
<Drawer.Screen
|
||||||
|
name="todos"
|
||||||
|
options=\{{
|
||||||
|
headerTitle: "Todos",
|
||||||
|
drawerLabel: "Todos",
|
||||||
|
drawerIcon: ({ size, color }) => (
|
||||||
|
<Ionicons name="checkbox-outline" size={size} color={color} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (includes examples "ai")}}
|
||||||
|
<Drawer.Screen
|
||||||
|
name="ai"
|
||||||
|
options=\{{
|
||||||
|
headerTitle: "AI",
|
||||||
|
drawerLabel: "AI",
|
||||||
|
drawerIcon: ({ size, color }) => (
|
||||||
|
<Ionicons
|
||||||
|
name="chatbubble-ellipses-outline"
|
||||||
|
size={size}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,52 +26,65 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<ScrollView className="py-4 flex-1">
|
<ScrollView showsVerticalScrollIndicator={false} className="flex-1">
|
||||||
<Text className="font-mono text-foreground text-2xl font-bold mb-6">
|
<Text className="font-mono text-foreground text-3xl font-bold mb-4">
|
||||||
BETTER T STACK
|
BETTER T STACK
|
||||||
</Text>
|
</Text>
|
||||||
|
<View className="bg-card border border-border rounded-xl p-6 mb-6 shadow-sm">
|
||||||
<View className="rounded-lg border border-foreground p-4">
|
|
||||||
<Text className="mb-2 font-medium text-foreground">API Status</Text>
|
|
||||||
{{#if (eq backend "convex")}}
|
{{#if (eq backend "convex")}}
|
||||||
<View className="flex-row items-center gap-2">
|
<View className="flex-row items-center gap-3">
|
||||||
<View
|
<View
|
||||||
className={`h-2.5 w-2.5 rounded-full ${
|
className={`h-3 w-3 rounded-full ${
|
||||||
healthCheck ? "bg-green-500" : "bg-red-500"
|
healthCheck ? "bg-green-500" : "bg-orange-500"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<Text className="text-sm text-foreground">
|
<View className="flex-1">
|
||||||
|
<Text className="text-sm font-medium text-card-foreground">
|
||||||
|
Convex
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs text-muted-foreground">
|
||||||
{healthCheck === undefined
|
{healthCheck === undefined
|
||||||
? "Checking..."
|
? "Checking connection..."
|
||||||
: healthCheck === "OK"
|
: healthCheck === "OK"
|
||||||
? "Connected"
|
? "All systems operational"
|
||||||
: "Error"}
|
: "Service unavailable"}
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#unless (eq api "none")}}
|
{{#unless (eq api "none")}}
|
||||||
<View className="flex-row items-center gap-2">
|
<View className="flex-row items-center gap-3">
|
||||||
<View
|
<View
|
||||||
className={`h-2.5 w-2.5 rounded-full ${
|
className={`h-3 w-3 rounded-full ${
|
||||||
healthCheck.data ? "bg-green-500" : "bg-red-500"
|
healthCheck.data ? "bg-green-500" : "bg-orange-500"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<Text className="text-sm text-foreground">
|
<View className="flex-1">
|
||||||
{{#if (eq api "orpc")}}
|
<Text className="text-sm font-medium text-card-foreground">
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
ORPC
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
|
TRPC
|
||||||
|
{{/if}}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs text-muted-foreground">
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
{healthCheck.isLoading
|
{healthCheck.isLoading
|
||||||
? "Checking..."
|
? "Checking connection..."
|
||||||
: healthCheck.data
|
: healthCheck.data
|
||||||
? "Connected"
|
? "All systems operational"
|
||||||
: "Disconnected"}
|
: "Service unavailable"}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if (eq api "trpc")}}
|
{{#if (eq api "trpc")}}
|
||||||
{healthCheck.isLoading
|
{healthCheck.isLoading
|
||||||
? "Checking..."
|
? "Checking connection..."
|
||||||
: healthCheck.data
|
: healthCheck.data
|
||||||
? "Connected"
|
? "All systems operational"
|
||||||
: "Disconnected"}
|
: "Service unavailable"}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
import { Link, Stack } from 'expo-router';
|
import { Container } from "@/components/container";
|
||||||
import { Text } from 'react-native';
|
import { Link, Stack } from "expo-router";
|
||||||
|
import { Text, View } from "react-native";
|
||||||
import { Container } from '@/components/container';
|
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
export default function NotFoundScreen() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
<Stack.Screen options={{ title: "Oops!" }} />
|
||||||
<Container>
|
<Container>
|
||||||
<Text className="text-xl font-bold">This screen doesn't exist.</Text>
|
<View className="flex-1 justify-center items-center p-6">
|
||||||
<Link href="/" className="mt-4 pt-4">
|
<View className="items-center">
|
||||||
<Text className="text-base text-[#2e78b7]">Go to home screen!</Text>
|
<Text className="text-6xl mb-4">🤔</Text>
|
||||||
</Link>
|
<Text className="text-2xl font-bold text-foreground mb-2 text-center">
|
||||||
|
Page Not Found
|
||||||
|
</Text>
|
||||||
|
<Text className="text-muted-foreground text-center mb-8 max-w-sm">
|
||||||
|
Sorry, the page you're looking for doesn't exist.
|
||||||
|
</Text>
|
||||||
|
<Link href="/" asChild>
|
||||||
|
<Text className="text-primary font-medium bg-primary/10 px-6 py-3 rounded-lg">
|
||||||
|
Go to Home
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
{{#if (includes examples "ai")}}
|
||||||
|
import "@/polyfills";
|
||||||
|
{{/if}}
|
||||||
{{#if (eq backend "convex")}}
|
{{#if (eq backend "convex")}}
|
||||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { Container } from "@/components/container";
|
|||||||
import { Text, View } from "react-native";
|
import { Text, View } from "react-native";
|
||||||
|
|
||||||
export default function Modal() {
|
export default function Modal() {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<View className="flex-1 justify-center items-center">
|
<View className="flex-1 p-6">
|
||||||
<Text className="text-xl font-bold text-foreground">Modal View</Text>
|
<View className="flex-row items-center justify-between mb-8">
|
||||||
</View>
|
<Text className="text-2xl font-bold text-foreground">Modal</Text>
|
||||||
</Container>
|
</View>
|
||||||
);
|
</View>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
import { SafeAreaView } from "react-native";
|
import { SafeAreaView } from "react-native";
|
||||||
|
|
||||||
export const Container = ({ children }: { children: React.ReactNode }) => {
|
export const Container = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex flex-1 p-4 bg-background">
|
<SafeAreaView className="flex-1 bg-background">{children}</SafeAreaView>
|
||||||
{children}
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,26 @@
|
|||||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from "react";
|
||||||
import { Pressable, StyleSheet } from 'react-native';
|
import { Pressable } from "react-native";
|
||||||
|
|
||||||
export const HeaderButton = forwardRef<typeof Pressable, { onPress?: () => void }>(
|
export const HeaderButton = forwardRef<
|
||||||
({ onPress }, ref) => {
|
typeof Pressable,
|
||||||
return (
|
{ onPress?: () => void }
|
||||||
<Pressable onPress={onPress}>
|
>(({ onPress }, ref) => {
|
||||||
{({ pressed }) => (
|
return (
|
||||||
<FontAwesome
|
<Pressable
|
||||||
name="info-circle"
|
onPress={onPress}
|
||||||
size={25}
|
className="p-2 mr-2 rounded-lg bg-secondary/50 active:bg-secondary"
|
||||||
color="gray"
|
>
|
||||||
style={[
|
{({ pressed }) => (
|
||||||
styles.headerRight,
|
<FontAwesome
|
||||||
{
|
name="info-circle"
|
||||||
opacity: pressed ? 0.5 : 1,
|
size={20}
|
||||||
},
|
className="text-secondary-foreground"
|
||||||
]}
|
style={{
|
||||||
/>
|
opacity: pressed ? 0.7 : 1,
|
||||||
)}
|
}}
|
||||||
</Pressable>
|
/>
|
||||||
);
|
)}
|
||||||
}
|
</Pressable>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const styles = StyleSheet.create({
|
|
||||||
headerRight: {
|
|
||||||
marginRight: 15,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||||
import { StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
export const TabBarIcon = (props: {
|
export const TabBarIcon = (props: {
|
||||||
name: React.ComponentProps<typeof FontAwesome>['name'];
|
name: React.ComponentProps<typeof FontAwesome>["name"];
|
||||||
color: string;
|
color: string;
|
||||||
}) => {
|
}) => {
|
||||||
return <FontAwesome size={28} style={styles.tabBarIcon} {...props} />;
|
return <FontAwesome size={24} style={{ marginBottom: -3 }} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const styles = StyleSheet.create({
|
|
||||||
tabBarIcon: {
|
|
||||||
marginBottom: -3,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -5,21 +5,46 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 240 10% 3.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
--primary: 240 5.9% 10%;
|
--card: 0 0% 100%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
--secondary: 240 4.8% 95.9%;
|
--popover: 0 0% 100%;
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 221.2 83.2% 53.3%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96%;
|
||||||
|
--secondary-foreground: 222.2 84% 4.9%;
|
||||||
|
--muted: 210 40% 96%;
|
||||||
|
--muted-foreground: 215.4 16.3% 40%;
|
||||||
|
--accent: 210 40% 96%;
|
||||||
|
--accent-foreground: 222.2 84% 4.9%;
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 221.2 83.2% 53.3%;
|
||||||
|
--radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark:root {
|
.dark:root {
|
||||||
--background: 240 10% 3.9%;
|
--background: 222.2 84% 4.9%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 210 40% 98%;
|
||||||
--primary: 0 0% 98%;
|
--card: 222.2 84% 4.9%;
|
||||||
--primary-foreground: 240 5.9% 10%;
|
--card-foreground: 210 40% 98%;
|
||||||
--secondary: 240 3.7% 15.9%;
|
--popover: 222.2 84% 4.9%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 217.2 91.2% 59.8%;
|
||||||
|
--primary-foreground: 222.2 84% 4.9%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 70%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
--destructive: 0 72% 51%;
|
--destructive: 0 72% 51%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 224.3 76.3% 94.1%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
export const NAV_THEME = {
|
export const NAV_THEME = {
|
||||||
light: {
|
light: {
|
||||||
background: "hsl(0 0% 100%)",
|
background: "hsl(0 0% 100%)",
|
||||||
border: "hsl(240 5.9% 90%)",
|
border: "hsl(220 13% 91%)",
|
||||||
card: "hsl(0 0% 100%)",
|
card: "hsl(0 0% 100%)",
|
||||||
notification: "hsl(0 84.2% 60.2%)",
|
notification: "hsl(0 84.2% 60.2%)",
|
||||||
primary: "hsl(240 5.9% 10%)",
|
primary: "hsl(221.2 83.2% 53.3%)",
|
||||||
text: "hsl(240 10% 3.9%)",
|
text: "hsl(222.2 84% 4.9%)",
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
background: "hsl(240 10% 3.9%)",
|
background: "hsl(222.2 84% 4.9%)",
|
||||||
border: "hsl(240 3.7% 15.9%)",
|
border: "hsl(217.2 32.6% 17.5%)",
|
||||||
card: "hsl(240 10% 3.9%)",
|
card: "hsl(222.2 84% 4.9%)",
|
||||||
notification: "hsl(0 72% 51%)",
|
notification: "hsl(0 72% 51%)",
|
||||||
primary: "hsl(0 0% 98%)",
|
primary: "hsl(217.2 91.2% 59.8%)",
|
||||||
text: "hsl(0 0% 98%)",
|
text: "hsl(210 40% 98%)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@tanstack/react-form": "^1.0.5",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"@tanstack/react-query": "^5.69.2",
|
"@tanstack/react-query": "^5.69.2",
|
||||||
|
{{#if (includes examples "ai")}}
|
||||||
|
"@stardazed/streams-text-encoding": "^1.0.2",
|
||||||
|
"@ungap/structured-clone": "^1.3.0",
|
||||||
|
{{/if}}
|
||||||
"expo": "^53.0.4",
|
"expo": "^53.0.4",
|
||||||
"expo-constants": "~17.1.4",
|
"expo-constants": "~17.1.4",
|
||||||
"expo-linking": "~7.1.4",
|
"expo-linking": "~7.1.4",
|
||||||
@@ -11,6 +11,14 @@ module.exports = {
|
|||||||
colors: {
|
colors: {
|
||||||
background: "hsl(var(--background))",
|
background: "hsl(var(--background))",
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: "hsl(var(--foreground))",
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: "hsl(var(--primary))",
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
@@ -19,9 +27,28 @@ module.exports = {
|
|||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
},
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
},
|
},
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
radius: "var(--radius)",
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
xl: "calc(var(--radius) + 4px)",
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
},
|
},
|
||||||
borderWidth: {
|
borderWidth: {
|
||||||
hairline: hairlineWidth(),
|
hairline: hairlineWidth(),
|
||||||
|
|||||||
@@ -10,23 +10,28 @@ export default function TabLayout() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
|
tabBarActiveTintColor: theme.colors.primary,
|
||||||
|
tabBarInactiveTintColor: theme.colors.mutedForeground,
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
|
borderTopColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
title: "Tab One",
|
title: "Home",
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="two"
|
name="two"
|
||||||
options={{
|
options={{
|
||||||
title: "Tab Two",
|
title: "Explore",
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
tabBarIcon: ({ color }) => (
|
||||||
|
<TabBarIcon name="compass" color={color} />
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,29 +1,37 @@
|
|||||||
import { Stack } from "expo-router";
|
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { Text, View } from "react-native";
|
import { ScrollView, Text, View } from "react-native";
|
||||||
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<Container>
|
||||||
<Stack.Screen options={{ title: "Tab One" }} />
|
<ScrollView contentContainerStyle={styles.container}>
|
||||||
<Container>
|
<View style={styles.headerSection}>
|
||||||
<View style={styles.container}>
|
<Text style={styles.title}>Tab One</Text>
|
||||||
<Text style={styles.text}>Tab One</Text>
|
<Text style={styles.subtitle}>
|
||||||
|
Explore the first section of your app
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Container>
|
</ScrollView>
|
||||||
</>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
text: {
|
|
||||||
color: theme.colors.typography,
|
|
||||||
},
|
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
padding: theme.spacing.lg,
|
||||||
paddingBottom: 100,
|
},
|
||||||
justifyContent: "center",
|
headerSection: {
|
||||||
alignItems: "center",
|
paddingVertical: theme.spacing.xl,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: theme.fontSize["3xl"],
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: theme.fontSize.lg,
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,29 +1,37 @@
|
|||||||
import { Stack } from "expo-router";
|
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { Text, View } from "react-native";
|
import { ScrollView, Text, View } from "react-native";
|
||||||
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
|
||||||
export default function Home() {
|
export default function TabTwo() {
|
||||||
return (
|
return (
|
||||||
<>
|
<Container>
|
||||||
<Stack.Screen options={{ title: "Tab Two" }} />
|
<ScrollView contentContainerStyle={styles.container}>
|
||||||
<Container>
|
<View style={styles.headerSection}>
|
||||||
<View style={styles.container}>
|
<Text style={styles.title}>Tab Two</Text>
|
||||||
<Text style={styles.text}>Tab Two</Text>
|
<Text style={styles.subtitle}>
|
||||||
|
Discover more features and content
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Container>
|
</ScrollView>
|
||||||
</>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
text: {
|
|
||||||
color: theme.colors.typography,
|
|
||||||
},
|
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
padding: theme.spacing.lg,
|
||||||
paddingBottom: 100,
|
},
|
||||||
justifyContent: "center",
|
headerSection: {
|
||||||
alignItems: "center",
|
paddingVertical: theme.spacing.xl,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: theme.fontSize["3xl"],
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: theme.fontSize.lg,
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -10,26 +10,26 @@ const DrawerLayout = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
screenOptions={{
|
screenOptions=\{{
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
color: theme.colors.typography,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
headerTintColor: theme.colors.typography,
|
headerTintColor: theme.colors.foreground,
|
||||||
drawerStyle: {
|
drawerStyle: {
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
drawerLabelStyle: {
|
drawerLabelStyle: {
|
||||||
color: theme.colors.typography,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
drawerInactiveTintColor: theme.colors.typography,
|
drawerInactiveTintColor: theme.colors.mutedForeground,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options=\{{
|
||||||
headerTitle: "Home",
|
headerTitle: "Home",
|
||||||
drawerLabel: "Home",
|
drawerLabel: "Home",
|
||||||
drawerIcon: ({ size, color }) => (
|
drawerIcon: ({ size, color }) => (
|
||||||
@@ -39,7 +39,7 @@ const DrawerLayout = () => {
|
|||||||
/>
|
/>
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
name="(tabs)"
|
name="(tabs)"
|
||||||
options={{
|
options=\{{
|
||||||
headerTitle: "Tabs",
|
headerTitle: "Tabs",
|
||||||
drawerLabel: "Tabs",
|
drawerLabel: "Tabs",
|
||||||
drawerIcon: ({ size, color }) => (
|
drawerIcon: ({ size, color }) => (
|
||||||
@@ -52,6 +52,34 @@ const DrawerLayout = () => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{{#if (includes examples "todo")}}
|
||||||
|
<Drawer.Screen
|
||||||
|
name="todos"
|
||||||
|
options=\{{
|
||||||
|
headerTitle: "Todos",
|
||||||
|
drawerLabel: "Todos",
|
||||||
|
drawerIcon: ({ size, color }) => (
|
||||||
|
<Ionicons name="checkbox-outline" size={size} color={color} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (includes examples "ai")}}
|
||||||
|
<Drawer.Screen
|
||||||
|
name="ai"
|
||||||
|
options=\{{
|
||||||
|
headerTitle: "AI",
|
||||||
|
drawerLabel: "AI",
|
||||||
|
drawerIcon: ({ size, color }) => (
|
||||||
|
<Ionicons
|
||||||
|
name="chatbubble-ellipses-outline"
|
||||||
|
size={size}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -28,56 +28,79 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<ScrollView contentContainerStyle={styles.pageContainer}>
|
<ScrollView
|
||||||
<Text style={styles.headerTitle}>BETTER T STACK</Text>
|
contentContainerStyle={styles.container}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<Text className="font-mono text-foreground text-3xl font-bold mb-4">
|
||||||
|
BETTER T STACK
|
||||||
|
</Text>
|
||||||
|
<View style={styles.statusCard}>
|
||||||
|
<View style={styles.statusHeader}>
|
||||||
|
<Text style={styles.statusTitle}>System Status</Text>
|
||||||
|
<View style={styles.statusBadge}>
|
||||||
|
<Text style={styles.statusBadgeText}>LIVE</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.apiStatusCard}>
|
|
||||||
<Text style={styles.cardTitle}>API Status</Text>
|
|
||||||
{{#if (eq backend "convex")}}
|
{{#if (eq backend "convex")}}
|
||||||
<View style={styles.apiStatusRow}>
|
<View style={styles.statusRow}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.statusIndicatorDot,
|
styles.statusDot,
|
||||||
healthCheck === "OK"
|
healthCheck === "OK"
|
||||||
? styles.statusIndicatorGreen
|
? styles.statusDotSuccess
|
||||||
: styles.statusIndicatorRed,
|
: styles.statusDotWarning,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.statusText}>
|
<View style={styles.statusContent}>
|
||||||
{healthCheck === undefined
|
<Text style={styles.statusLabel}>Convex</Text>
|
||||||
? "Checking..."
|
<Text style={styles.statusDescription}>
|
||||||
: healthCheck === "OK"
|
{healthCheck === undefined
|
||||||
? "Connected"
|
? "Checking connection..."
|
||||||
: "Error"}
|
: healthCheck === "OK"
|
||||||
</Text>
|
? "All systems operational"
|
||||||
|
: "Service unavailable"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#unless (eq api "none")}}
|
{{#unless (eq api "none")}}
|
||||||
<View style={styles.apiStatusRow}>
|
<View style={styles.statusRow}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.statusIndicatorDot,
|
styles.statusDot,
|
||||||
healthCheck.data
|
healthCheck.data
|
||||||
? styles.statusIndicatorGreen
|
? styles.statusDotSuccess
|
||||||
: styles.statusIndicatorRed,
|
: styles.statusDotWarning,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.statusText}>
|
<View style={styles.statusContent}>
|
||||||
{{#if (eq api "orpc")}}
|
<Text style={styles.statusLabel}>
|
||||||
{healthCheck.isLoading
|
{{#if (eq api "orpc")}}
|
||||||
? "Checking..."
|
ORPC
|
||||||
: healthCheck.data
|
{{/if}}
|
||||||
? "Connected"
|
{{#if (eq api "trpc")}}
|
||||||
: "Disconnected"}
|
TRPC
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if (eq api "trpc")}}
|
</Text>
|
||||||
{healthCheck.isLoading
|
<Text style={styles.statusDescription}>
|
||||||
? "Checking..."
|
{{#if (eq api "orpc")}}
|
||||||
: healthCheck.data
|
{healthCheck.isLoading
|
||||||
? "Connected"
|
? "Checking connection..."
|
||||||
: "Disconnected"}
|
: healthCheck.data
|
||||||
{{/if}}
|
? "All systems operational"
|
||||||
</Text>
|
: "Service unavailable"}
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
|
{healthCheck.isLoading
|
||||||
|
? "Checking connection..."
|
||||||
|
: healthCheck.data
|
||||||
|
? "All systems operational"
|
||||||
|
: "Service unavailable"}
|
||||||
|
{{/if}}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -88,44 +111,84 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
pageContainer: {
|
container: {
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: theme.spacing.md,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
heroSection: {
|
||||||
color: theme?.colors?.typography,
|
paddingVertical: theme.spacing.xl,
|
||||||
fontSize: 30,
|
},
|
||||||
|
heroTitle: {
|
||||||
|
fontSize: theme.fontSize["4xl"],
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
marginBottom: 16,
|
color: theme.colors.foreground,
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
},
|
},
|
||||||
apiStatusCard: {
|
heroSubtitle: {
|
||||||
marginBottom: 24,
|
fontSize: theme.fontSize.lg,
|
||||||
borderRadius: 8,
|
color: theme.colors.mutedForeground,
|
||||||
|
lineHeight: 28,
|
||||||
|
},
|
||||||
|
statusCard: {
|
||||||
|
backgroundColor: theme.colors.card,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: theme?.colors?.border,
|
borderColor: theme.colors.border,
|
||||||
padding: 16,
|
borderRadius: theme.borderRadius.xl,
|
||||||
|
padding: theme.spacing.lg,
|
||||||
|
marginBottom: theme.spacing.lg,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 3,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
cardTitle: {
|
statusHeader: {
|
||||||
marginBottom: 12,
|
|
||||||
fontWeight: "500",
|
|
||||||
color: theme?.colors?.typography,
|
|
||||||
},
|
|
||||||
apiStatusRow: {
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 8,
|
justifyContent: "space-between",
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
},
|
},
|
||||||
statusIndicatorDot: {
|
statusTitle: {
|
||||||
height: 12,
|
fontSize: theme.fontSize.lg,
|
||||||
width: 12,
|
fontWeight: "600",
|
||||||
|
color: theme.colors.cardForeground,
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
backgroundColor: theme.colors.secondary,
|
||||||
|
paddingHorizontal: theme.spacing.sm + 4,
|
||||||
|
paddingVertical: theme.spacing.xs,
|
||||||
borderRadius: 9999,
|
borderRadius: 9999,
|
||||||
},
|
},
|
||||||
statusIndicatorGreen: {
|
statusBadgeText: {
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: theme.colors.secondaryForeground,
|
||||||
|
},
|
||||||
|
statusRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing.sm + 4,
|
||||||
|
},
|
||||||
|
statusDot: {
|
||||||
|
height: 12,
|
||||||
|
width: 12,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
statusDotSuccess: {
|
||||||
backgroundColor: theme.colors.success,
|
backgroundColor: theme.colors.success,
|
||||||
},
|
},
|
||||||
statusIndicatorRed: {
|
statusDotWarning: {
|
||||||
backgroundColor: theme.colors.destructive,
|
backgroundColor: "#F59E0B",
|
||||||
},
|
},
|
||||||
statusText: {
|
statusContent: {
|
||||||
color: theme?.colors?.typography,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
statusLabel: {
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: theme.colors.cardForeground,
|
||||||
|
},
|
||||||
|
statusDescription: {
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,34 +1,65 @@
|
|||||||
import { Link, Stack } from "expo-router";
|
|
||||||
import { Text } from "react-native";
|
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
|
||||||
|
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
|
import { Link, Stack } from "expo-router";
|
||||||
|
import { Text, View } from "react-native";
|
||||||
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
export default function NotFoundScreen() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ title: "Oops!" }} />
|
<Stack.Screen options={{ title: "Oops!" }} />
|
||||||
<Container>
|
<Container>
|
||||||
<Text style={styles.title}>This screen doesn't exist.</Text>
|
<View style={styles.container}>
|
||||||
<Link href="/" style={styles.link}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.linkText}>Go to home screen!</Text>
|
<Text style={styles.emoji}>🤔</Text>
|
||||||
</Link>
|
<Text style={styles.title}>Page Not Found</Text>
|
||||||
|
<Text style={styles.description}>
|
||||||
|
Sorry, the page you're looking for doesn't exist.
|
||||||
|
</Text>
|
||||||
|
<Link href="/" style={styles.button}>
|
||||||
|
<Text style={styles.buttonText}>Go to Home</Text>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
emoji: {
|
||||||
|
fontSize: 64,
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 20,
|
fontSize: theme.fontSize["2xl"],
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: theme.colors.typography,
|
color: theme.colors.foreground,
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
link: {
|
description: {
|
||||||
marginTop: 16,
|
color: theme.colors.mutedForeground,
|
||||||
paddingVertical: 16,
|
textAlign: "center",
|
||||||
|
marginBottom: theme.spacing.xl,
|
||||||
|
maxWidth: 280,
|
||||||
},
|
},
|
||||||
linkText: {
|
button: {
|
||||||
fontSize: 14,
|
backgroundColor: `${theme.colors.primary}1A`, // 10% opacity
|
||||||
|
paddingHorizontal: theme.spacing.lg,
|
||||||
|
paddingVertical: theme.spacing.sm + 4,
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: theme.colors.primary,
|
||||||
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
{{#if (includes examples "ai")}}
|
||||||
|
import "@/polyfills";
|
||||||
|
{{/if}}
|
||||||
{{#if (eq api "trpc")}}
|
{{#if (eq api "trpc")}}
|
||||||
import { queryClient } from "@/utils/trpc";
|
import { queryClient } from "@/utils/trpc";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -39,9 +42,9 @@ export default function RootLayout() {
|
|||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
color: theme.colors.typography,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
headerTintColor: theme.colors.typography,
|
headerTintColor: theme.colors.foreground,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
||||||
@@ -62,9 +65,9 @@ export default function RootLayout() {
|
|||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
color: theme.colors.typography,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
headerTintColor: theme.colors.typography,
|
headerTintColor: theme.colors.foreground,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
||||||
@@ -83,9 +86,9 @@ export default function RootLayout() {
|
|||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
color: theme.colors.typography,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
headerTintColor: theme.colors.typography,
|
headerTintColor: theme.colors.foreground,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { Text, View } from "react-native";
|
||||||
import { Platform, Text, View } from "react-native";
|
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
|
||||||
export default function Modal() {
|
export default function Modal() {
|
||||||
return (
|
return (
|
||||||
<>
|
<Container>
|
||||||
<StatusBar style={Platform.OS === "ios" ? "light" : "auto"} />
|
<View style={styles.container}>
|
||||||
<Container>
|
<View style={styles.header}>
|
||||||
<View style={styles.container}>
|
<Text style={styles.title}>Modal</Text>
|
||||||
<Text style={styles.text}>Model</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</Container>
|
</View>
|
||||||
</>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
text: {
|
|
||||||
color: theme.colors.typography,
|
|
||||||
},
|
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingBottom: 100,
|
padding: theme.spacing.lg,
|
||||||
justifyContent: "center",
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: theme.spacing.xl,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: theme.fontSize["2xl"],
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native";
|
import { SafeAreaView } from "react-native";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
|
||||||
export const Container = ({ children }: { children: React.ReactNode }) => {
|
export const Container = ({ children }: { children: React.ReactNode }) => {
|
||||||
return <View style={styles.container}>{children}</View>;
|
return <SafeAreaView style={styles.container}>{children}</SafeAreaView>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme, rt) => ({
|
const styles = StyleSheet.create((theme, rt) => ({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingBottom: rt.insets.bottom,
|
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
transform: [
|
paddingBottom: rt.insets.bottom,
|
||||||
{
|
|
||||||
translateY: rt.insets.ime * -1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,31 +1,36 @@
|
|||||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from "react";
|
||||||
import { Pressable, StyleSheet } from 'react-native';
|
import { Pressable } from "react-native";
|
||||||
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
|
||||||
export const HeaderButton = forwardRef<typeof Pressable, { onPress?: () => void }>(
|
export const HeaderButton = forwardRef<
|
||||||
({ onPress }, ref) => {
|
typeof Pressable,
|
||||||
return (
|
{ onPress?: () => void }
|
||||||
<Pressable onPress={onPress}>
|
>(({ onPress }, ref) => {
|
||||||
{({ pressed }) => (
|
return (
|
||||||
<FontAwesome
|
<Pressable onPress={onPress} style={styles.button}>
|
||||||
name="info-circle"
|
{({ pressed }) => (
|
||||||
size={25}
|
<FontAwesome
|
||||||
color="gray"
|
name="info-circle"
|
||||||
style={[
|
size={20}
|
||||||
styles.headerRight,
|
color={styles.icon.color}
|
||||||
{
|
style={{
|
||||||
opacity: pressed ? 0.5 : 1,
|
opacity: pressed ? 0.7 : 1,
|
||||||
},
|
}}
|
||||||
]}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</Pressable>
|
||||||
</Pressable>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const styles = StyleSheet.create({
|
|
||||||
headerRight: {
|
|
||||||
marginRight: 15,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
button: {
|
||||||
|
padding: theme.spacing.sm,
|
||||||
|
marginRight: theme.spacing.sm,
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
backgroundColor: `${theme.colors.secondary}80`, // 50% opacity
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
color: theme.colors.secondaryForeground,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||||
import { StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
export const TabBarIcon = (props: {
|
export const TabBarIcon = (props: {
|
||||||
name: React.ComponentProps<typeof FontAwesome>['name'];
|
name: React.ComponentProps<typeof FontAwesome>["name"];
|
||||||
color: string;
|
color: string;
|
||||||
}) => {
|
}) => {
|
||||||
return <FontAwesome size={28} style={styles.tabBarIcon} {...props} />;
|
return <FontAwesome size={24} style={{ marginBottom: -3 }} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const styles = StyleSheet.create({
|
|
||||||
tabBarIcon: {
|
|
||||||
marginBottom: -3,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
"@react-navigation/bottom-tabs": "^7.0.5",
|
"@react-navigation/bottom-tabs": "^7.0.5",
|
||||||
"@react-navigation/drawer": "^7.0.0",
|
"@react-navigation/drawer": "^7.0.0",
|
||||||
"@react-navigation/native": "^7.0.3",
|
"@react-navigation/native": "^7.0.3",
|
||||||
|
{{#if (includes examples "ai")}}
|
||||||
|
"@stardazed/streams-text-encoding": "^1.0.2",
|
||||||
|
"@ungap/structured-clone": "^1.3.0",
|
||||||
|
{{/if}}
|
||||||
"@tanstack/react-form": "^1.0.5",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417",
|
"babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417",
|
||||||
"expo": "^53.0.8",
|
"expo": "^53.0.8",
|
||||||
@@ -34,7 +38,7 @@
|
|||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.10.0",
|
"react-native-screens": "~4.10.0",
|
||||||
"react-native-unistyles": "3.0.0-rc.3",
|
"react-native-unistyles": "^3.0.0-rc.4",
|
||||||
"react-native-web": "^0.20.0"
|
"react-native-web": "^0.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1,35 +1,98 @@
|
|||||||
const sharedColors = {
|
const sharedColors = {
|
||||||
success: "#22C55E",
|
success: "#22C55E",
|
||||||
destructive: "#DC2626",
|
destructive: "#EF4444",
|
||||||
border: "#D1D5DB",
|
warning: "#F59E0B",
|
||||||
|
info: "#3B82F6",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const lightTheme = {
|
export const lightTheme = {
|
||||||
colors: {
|
colors: {
|
||||||
...sharedColors,
|
...sharedColors,
|
||||||
typography: "#000000",
|
typography: "hsl(222.2 84% 4.9%)",
|
||||||
background: "#ffffff",
|
background: "hsl(0 0% 100%)",
|
||||||
primary: "#3B82F6",
|
foreground: "hsl(222.2 84% 4.9%)",
|
||||||
|
card: "hsl(0 0% 100%)",
|
||||||
|
cardForeground: "hsl(222.2 84% 4.9%)",
|
||||||
|
primary: "hsl(221.2 83.2% 53.3%)",
|
||||||
|
primaryForeground: "hsl(210 40% 98%)",
|
||||||
|
secondary: "hsl(210 40% 96%)",
|
||||||
|
secondaryForeground: "hsl(222.2 84% 4.9%)",
|
||||||
|
muted: "hsl(210 40% 96%)",
|
||||||
|
mutedForeground: "hsl(215.4 16.3% 46.9%)",
|
||||||
|
accent: "hsl(210 40% 96%)",
|
||||||
|
accentForeground: "hsl(222.2 84% 4.9%)",
|
||||||
|
border: "hsl(214.3 31.8% 91.4%)",
|
||||||
|
input: "hsl(214.3 31.8% 91.4%)",
|
||||||
|
ring: "hsl(221.2 83.2% 53.3%)",
|
||||||
},
|
},
|
||||||
margins: {
|
spacing: {
|
||||||
sm: 2,
|
xs: 4,
|
||||||
md: 4,
|
sm: 8,
|
||||||
lg: 8,
|
md: 16,
|
||||||
xl: 12,
|
lg: 24,
|
||||||
|
xl: 32,
|
||||||
|
xxl: 48,
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
sm: 6,
|
||||||
|
md: 8,
|
||||||
|
lg: 12,
|
||||||
|
xl: 16,
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
xs: 12,
|
||||||
|
sm: 14,
|
||||||
|
base: 16,
|
||||||
|
lg: 18,
|
||||||
|
xl: 20,
|
||||||
|
"2xl": 24,
|
||||||
|
"3xl": 30,
|
||||||
|
"4xl": 36,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const darkTheme = {
|
export const darkTheme = {
|
||||||
colors: {
|
colors: {
|
||||||
...sharedColors,
|
...sharedColors,
|
||||||
typography: "#ffffff",
|
typography: "hsl(210 40% 98%)",
|
||||||
background: "#000000",
|
background: "hsl(222.2 84% 4.9%)",
|
||||||
primary: "#60A5FA",
|
foreground: "hsl(210 40% 98%)",
|
||||||
|
card: "hsl(222.2 84% 4.9%)",
|
||||||
|
cardForeground: "hsl(210 40% 98%)",
|
||||||
|
primary: "hsl(217.2 91.2% 59.8%)",
|
||||||
|
primaryForeground: "hsl(222.2 84% 4.9%)",
|
||||||
|
secondary: "hsl(217.2 32.6% 17.5%)",
|
||||||
|
secondaryForeground: "hsl(210 40% 98%)",
|
||||||
|
muted: "hsl(217.2 32.6% 17.5%)",
|
||||||
|
mutedForeground: "hsl(215 20.2% 65.1%)",
|
||||||
|
accent: "hsl(217.2 32.6% 17.5%)",
|
||||||
|
accentForeground: "hsl(210 40% 98%)",
|
||||||
|
border: "hsl(217.2 32.6% 17.5%)",
|
||||||
|
input: "hsl(217.2 32.6% 17.5%)",
|
||||||
|
ring: "hsl(224.3 76.3% 94.1%)",
|
||||||
},
|
},
|
||||||
margins: {
|
spacing: {
|
||||||
sm: 2,
|
xs: 4,
|
||||||
md: 4,
|
sm: 8,
|
||||||
lg: 8,
|
md: 16,
|
||||||
xl: 12,
|
lg: 24,
|
||||||
|
xl: 32,
|
||||||
|
xxl: 48,
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
sm: 6,
|
||||||
|
md: 8,
|
||||||
|
lg: 12,
|
||||||
|
xl: 16,
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
xs: 12,
|
||||||
|
sm: 14,
|
||||||
|
base: 16,
|
||||||
|
lg: 18,
|
||||||
|
xl: 20,
|
||||||
|
"2xl": 24,
|
||||||
|
"3xl": 30,
|
||||||
|
"4xl": 36,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { StyleSheet } from 'react-native-unistyles';
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
|
||||||
import { breakpoints } from './breakpoints';
|
import { breakpoints } from "./breakpoints";
|
||||||
import { lightTheme, darkTheme } from './theme';
|
import { darkTheme, lightTheme } from "./theme";
|
||||||
|
|
||||||
type AppBreakpoints = typeof breakpoints;
|
type AppBreakpoints = typeof breakpoints;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ type AppThemes = {
|
|||||||
dark: typeof darkTheme;
|
dark: typeof darkTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module 'react-native-unistyles' {
|
declare module "react-native-unistyles" {
|
||||||
export interface UnistylesBreakpoints extends AppBreakpoints {}
|
export interface UnistylesBreakpoints extends AppBreakpoints {}
|
||||||
export interface UnistylesThemes extends AppThemes {}
|
export interface UnistylesThemes extends AppThemes {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -626,90 +626,35 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const incompatibleExamples: string[] = [];
|
const incompatibleExamples: string[] = [];
|
||||||
const isWeb = hasWebFrontend(nextStack.webFrontend);
|
|
||||||
const hasNative = checkHasNativeFrontend(nextStack.nativeFrontend);
|
|
||||||
const isNativeOnly = hasNative && !isWeb;
|
|
||||||
|
|
||||||
if (isNativeOnly) {
|
// Note: Examples are now supported with Native-only frontends
|
||||||
if (nextStack.examples.length > 0) {
|
if (
|
||||||
notes.webFrontend.notes.push(
|
nextStack.database === "none" &&
|
||||||
"Examples are not supported with Native-only frontend. Examples will be removed.",
|
nextStack.examples.includes("todo")
|
||||||
);
|
) {
|
||||||
notes.examples.notes.push(
|
incompatibleExamples.push("todo");
|
||||||
"Examples require a web frontend. They will be removed.",
|
changes.push({
|
||||||
);
|
category: "examples",
|
||||||
notes.webFrontend.hasIssue = true;
|
message: "Todo example removed (requires a database)",
|
||||||
notes.examples.hasIssue = true;
|
});
|
||||||
incompatibleExamples.push(...nextStack.examples);
|
}
|
||||||
changes.push({
|
if (nextStack.backend === "elysia" && nextStack.examples.includes("ai")) {
|
||||||
category: "examples",
|
incompatibleExamples.push("ai");
|
||||||
message:
|
changes.push({
|
||||||
"Examples removed (not supported with Native-only frontend)",
|
category: "examples",
|
||||||
});
|
message: "AI example removed (not compatible with Elysia)",
|
||||||
}
|
});
|
||||||
} else {
|
}
|
||||||
if (!isWeb) {
|
if (isSolid && nextStack.examples.includes("ai")) {
|
||||||
if (nextStack.examples.includes("todo")) {
|
incompatibleExamples.push("ai");
|
||||||
incompatibleExamples.push("todo");
|
changes.push({
|
||||||
changes.push({
|
category: "examples",
|
||||||
category: "examples",
|
message: "AI example removed (not compatible with Solid)",
|
||||||
message: "Todo example removed (requires web frontend)",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
if (nextStack.examples.includes("ai")) {
|
|
||||||
incompatibleExamples.push("ai");
|
|
||||||
changes.push({
|
|
||||||
category: "examples",
|
|
||||||
message: "AI example removed (requires web frontend)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
nextStack.database === "none" &&
|
|
||||||
nextStack.examples.includes("todo")
|
|
||||||
) {
|
|
||||||
incompatibleExamples.push("todo");
|
|
||||||
changes.push({
|
|
||||||
category: "examples",
|
|
||||||
message: "Todo example removed (requires a database)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
nextStack.backend === "elysia" &&
|
|
||||||
nextStack.examples.includes("ai")
|
|
||||||
) {
|
|
||||||
incompatibleExamples.push("ai");
|
|
||||||
changes.push({
|
|
||||||
category: "examples",
|
|
||||||
message: "AI example removed (not compatible with Elysia)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isSolid && nextStack.examples.includes("ai")) {
|
|
||||||
incompatibleExamples.push("ai");
|
|
||||||
changes.push({
|
|
||||||
category: "examples",
|
|
||||||
message: "AI example removed (not compatible with Solid)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)];
|
const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)];
|
||||||
if (uniqueIncompatibleExamples.length > 0) {
|
if (uniqueIncompatibleExamples.length > 0) {
|
||||||
if (!isWeb && !isNativeOnly) {
|
|
||||||
if (
|
|
||||||
uniqueIncompatibleExamples.includes("todo") ||
|
|
||||||
uniqueIncompatibleExamples.includes("ai")
|
|
||||||
) {
|
|
||||||
notes.webFrontend.notes.push(
|
|
||||||
"Examples require a web frontend. Incompatible examples will be removed.",
|
|
||||||
);
|
|
||||||
notes.examples.notes.push(
|
|
||||||
"Requires a web frontend. Incompatible examples will be removed.",
|
|
||||||
);
|
|
||||||
notes.webFrontend.hasIssue = true;
|
|
||||||
notes.examples.hasIssue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
nextStack.database === "none" &&
|
nextStack.database === "none" &&
|
||||||
uniqueIncompatibleExamples.includes("todo")
|
uniqueIncompatibleExamples.includes("todo")
|
||||||
@@ -769,7 +714,6 @@ const getCompatibilityRules = (stack: StackState) => {
|
|||||||
const isBackendNone = stack.backend === "none";
|
const isBackendNone = stack.backend === "none";
|
||||||
const hasWebFrontendSelected = hasWebFrontend(stack.webFrontend);
|
const hasWebFrontendSelected = hasWebFrontend(stack.webFrontend);
|
||||||
const hasNativeFrontend = checkHasNativeFrontend(stack.nativeFrontend);
|
const hasNativeFrontend = checkHasNativeFrontend(stack.nativeFrontend);
|
||||||
const hasNativeOnly = hasNativeFrontend && !hasWebFrontendSelected;
|
|
||||||
const hasSolid = stack.webFrontend.includes("solid");
|
const hasSolid = stack.webFrontend.includes("solid");
|
||||||
const hasNuxt = stack.webFrontend.includes("nuxt");
|
const hasNuxt = stack.webFrontend.includes("nuxt");
|
||||||
const hasSvelte = stack.webFrontend.includes("svelte");
|
const hasSvelte = stack.webFrontend.includes("svelte");
|
||||||
@@ -779,7 +723,6 @@ const getCompatibilityRules = (stack: StackState) => {
|
|||||||
isBackendNone,
|
isBackendNone,
|
||||||
hasWebFrontend: hasWebFrontendSelected,
|
hasWebFrontend: hasWebFrontendSelected,
|
||||||
hasNativeFrontend,
|
hasNativeFrontend,
|
||||||
hasNativeOnly,
|
|
||||||
hasPWACompatible: hasPWACompatibleFrontend(stack.webFrontend),
|
hasPWACompatible: hasPWACompatibleFrontend(stack.webFrontend),
|
||||||
hasTauriCompatible: hasTauriCompatibleFrontend(stack.webFrontend),
|
hasTauriCompatible: hasTauriCompatibleFrontend(stack.webFrontend),
|
||||||
hasNuxtOrSvelteOrSolid: hasNuxt || hasSvelte || hasSolid,
|
hasNuxtOrSvelteOrSolid: hasNuxt || hasSvelte || hasSolid,
|
||||||
@@ -1328,59 +1271,37 @@ const StackBuilder = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (catKey === "examples" && techId !== "none") {
|
if (catKey === "examples" && techId !== "none") {
|
||||||
if (rules.hasNativeOnly) {
|
if (stack.api === "none" && !rules.isConvex && !rules.isBackendNone) {
|
||||||
addRule(
|
addRule(
|
||||||
category,
|
category,
|
||||||
techId,
|
techId,
|
||||||
"Disabled: Examples are not supported with a Native-only frontend.",
|
"Disabled: Examples require an API. Cannot be selected when API is 'None'.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
stack.database === "none" &&
|
||||||
|
techId === "todo" &&
|
||||||
|
!rules.isConvex
|
||||||
|
) {
|
||||||
|
addRule(
|
||||||
|
category,
|
||||||
|
techId,
|
||||||
|
"Disabled: The 'Todo' example requires a database.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (stack.backend === "elysia" && techId === "ai") {
|
||||||
|
addRule(
|
||||||
|
category,
|
||||||
|
techId,
|
||||||
|
"Disabled: The 'AI' example is not compatible with an Elysia backend.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (rules.hasSolid && techId === "ai") {
|
||||||
|
addRule(
|
||||||
|
category,
|
||||||
|
techId,
|
||||||
|
"Disabled: The 'AI' example is not compatible with a Solid frontend.",
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
!rules.hasWebFrontend &&
|
|
||||||
(techId === "todo" || techId === "ai")
|
|
||||||
) {
|
|
||||||
addRule(
|
|
||||||
category,
|
|
||||||
techId,
|
|
||||||
"Disabled: This example requires a web frontend.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
stack.api === "none" &&
|
|
||||||
!rules.isConvex &&
|
|
||||||
!rules.isBackendNone
|
|
||||||
) {
|
|
||||||
addRule(
|
|
||||||
category,
|
|
||||||
techId,
|
|
||||||
"Disabled: Examples require an API. Cannot be selected when API is 'None'.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
stack.database === "none" &&
|
|
||||||
techId === "todo" &&
|
|
||||||
!rules.isConvex
|
|
||||||
) {
|
|
||||||
addRule(
|
|
||||||
category,
|
|
||||||
techId,
|
|
||||||
"Disabled: The 'Todo' example requires a database.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (stack.backend === "elysia" && techId === "ai") {
|
|
||||||
addRule(
|
|
||||||
category,
|
|
||||||
techId,
|
|
||||||
"Disabled: The 'AI' example is not compatible with an Elysia backend.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (rules.hasSolid && techId === "ai") {
|
|
||||||
addRule(
|
|
||||||
category,
|
|
||||||
techId,
|
|
||||||
"Disabled: The 'AI' example is not compatible with a Solid frontend.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user