Add core functionality for mobile post scheduling app
Adds necessary dependencies including axios and react-native-calendars to pnpm-lock.yaml. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7b0991ce-c7b8-4c82-9acc-fd3f9e762a01 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: dc1266fa-8375-43e1-aca0-9df31350f647 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/86064bd6-c937-4ca5-a5bf-bbef5749fb60/7b0991ce-c7b8-4c82-9acc-fd3f9e762a01/kWnlAIM Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { usePostiz } from "@/context/PostizContext";
|
||||
import { useColors } from "@/hooks/useColors";
|
||||
import axios from "axios";
|
||||
|
||||
const DEFAULT_BASE_URL = "https://postiz.gyozamancave.fr/public/v1";
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { apiKey, baseUrl, isConfigured, saveSettings, clearSettings } = usePostiz();
|
||||
|
||||
const [inputKey, setInputKey] = useState(apiKey);
|
||||
const [inputUrl, setInputUrl] = useState(baseUrl || DEFAULT_BASE_URL);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [validationStatus, setValidationStatus] = useState<
|
||||
"idle" | "ok" | "error"
|
||||
>("idle");
|
||||
|
||||
useEffect(() => {
|
||||
setInputKey(apiKey);
|
||||
setInputUrl(baseUrl || DEFAULT_BASE_URL);
|
||||
}, [apiKey, baseUrl]);
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (!inputKey.trim() || !inputUrl.trim()) {
|
||||
Alert.alert("Missing fields", "Please enter both API key and base URL.");
|
||||
return;
|
||||
}
|
||||
setValidating(true);
|
||||
setValidationStatus("idle");
|
||||
try {
|
||||
await axios.get(`${inputUrl.replace(/\/$/, "")}/integrations`, {
|
||||
headers: { Authorization: inputKey.trim() },
|
||||
timeout: 10000,
|
||||
});
|
||||
setValidationStatus("ok");
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
} catch {
|
||||
setValidationStatus("error");
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!inputKey.trim() || !inputUrl.trim()) {
|
||||
Alert.alert("Missing fields", "Please enter both API key and base URL.");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await saveSettings(inputKey.trim(), inputUrl.trim().replace(/\/$/, ""));
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
Alert.alert("Saved", "Settings saved successfully.");
|
||||
} catch {
|
||||
Alert.alert("Error", "Failed to save settings.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
Alert.alert(
|
||||
"Disconnect",
|
||||
"Remove your API key and disconnect from Postiz?",
|
||||
[
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Disconnect",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
await clearSettings();
|
||||
setInputKey("");
|
||||
setInputUrl(DEFAULT_BASE_URL);
|
||||
setValidationStatus("idle");
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAwareScrollView
|
||||
style={{ flex: 1, backgroundColor: colors.background }}
|
||||
contentContainerStyle={[
|
||||
styles.container,
|
||||
{
|
||||
paddingTop: Platform.OS === "web" ? 67 : 24,
|
||||
paddingBottom:
|
||||
Platform.OS === "web" ? 100 : insets.bottom + 40,
|
||||
},
|
||||
]}
|
||||
bottomOffset={60}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{!isConfigured && (
|
||||
<View style={[styles.banner, { backgroundColor: colors.primary + "18", borderColor: colors.primary + "40" }]}>
|
||||
<Feather name="info" size={16} color={colors.primary} />
|
||||
<Text style={[styles.bannerText, { color: colors.primary }]}>
|
||||
Connect to your Postiz instance to get started
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isConfigured && (
|
||||
<View style={[styles.connectedBadge, { backgroundColor: colors.success + "18", borderColor: colors.success + "40" }]}>
|
||||
<Feather name="check-circle" size={14} color={colors.success} />
|
||||
<Text style={[styles.connectedText, { color: colors.success }]}>
|
||||
Connected to Postiz
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.label, { color: colors.mutedForeground }]}>
|
||||
BASE URL
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrap,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Feather name="globe" size={16} color={colors.mutedForeground} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.foreground }]}
|
||||
placeholder="https://postiz.example.com/public/v1"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
value={inputUrl}
|
||||
onChangeText={(t) => {
|
||||
setInputUrl(t);
|
||||
setValidationStatus("idle");
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="url"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.label, { color: colors.mutedForeground }]}>
|
||||
API KEY
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrap,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Feather name="key" size={16} color={colors.mutedForeground} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.foreground }]}
|
||||
placeholder="Enter your API key"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
value={inputKey}
|
||||
onChangeText={(t) => {
|
||||
setInputKey(t);
|
||||
setValidationStatus("idle");
|
||||
}}
|
||||
secureTextEntry={!showKey}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowKey((v) => !v)} activeOpacity={0.7}>
|
||||
<Feather
|
||||
name={showKey ? "eye-off" : "eye"}
|
||||
size={16}
|
||||
color={colors.mutedForeground}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{validationStatus === "ok" && (
|
||||
<View style={styles.validationRow}>
|
||||
<Feather name="check-circle" size={13} color={colors.success} />
|
||||
<Text style={[styles.validationText, { color: colors.success }]}>
|
||||
Connection successful
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{validationStatus === "error" && (
|
||||
<View style={styles.validationRow}>
|
||||
<Feather name="x-circle" size={13} color={colors.error} />
|
||||
<Text style={[styles.validationText, { color: colors.error }]}>
|
||||
Could not connect. Check your URL and API key.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleValidate}
|
||||
activeOpacity={0.8}
|
||||
disabled={validating}
|
||||
style={[
|
||||
styles.validateBtn,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{validating ? (
|
||||
<ActivityIndicator color={colors.primary} size="small" />
|
||||
) : (
|
||||
<>
|
||||
<Feather name="wifi" size={15} color={colors.primary} />
|
||||
<Text style={[styles.validateText, { color: colors.primary }]}>
|
||||
Test Connection
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleSave}
|
||||
activeOpacity={0.85}
|
||||
disabled={saving}
|
||||
style={[
|
||||
styles.saveBtn,
|
||||
{ backgroundColor: saving ? colors.muted : colors.primary },
|
||||
]}
|
||||
>
|
||||
{saving ? (
|
||||
<ActivityIndicator color={colors.primaryForeground} size="small" />
|
||||
) : (
|
||||
<>
|
||||
<Feather name="save" size={15} color={colors.primaryForeground} />
|
||||
<Text style={[styles.saveText, { color: colors.primaryForeground }]}>
|
||||
Save Settings
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{isConfigured && (
|
||||
<TouchableOpacity
|
||||
onPress={handleClear}
|
||||
activeOpacity={0.8}
|
||||
style={[styles.clearBtn, { borderColor: colors.destructive + "60" }]}
|
||||
>
|
||||
<Feather name="log-out" size={14} color={colors.destructive} />
|
||||
<Text style={[styles.clearText, { color: colors.destructive }]}>
|
||||
Disconnect
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={[styles.footerText, { color: colors.mutedForeground }]}>
|
||||
Your API key is stored securely on this device and never transmitted to third parties.
|
||||
</Text>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 20,
|
||||
gap: 16,
|
||||
},
|
||||
banner: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
},
|
||||
bannerText: {
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter_500Medium",
|
||||
flex: 1,
|
||||
},
|
||||
connectedBadge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1,
|
||||
alignSelf: "flex-start",
|
||||
},
|
||||
connectedText: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter_600SemiBold",
|
||||
},
|
||||
section: {
|
||||
gap: 8,
|
||||
},
|
||||
label: {
|
||||
fontSize: 11,
|
||||
fontFamily: "Inter_600SemiBold",
|
||||
letterSpacing: 0.8,
|
||||
marginLeft: 2,
|
||||
},
|
||||
inputWrap: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
gap: 10,
|
||||
},
|
||||
inputIcon: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontFamily: "Inter_400Regular",
|
||||
},
|
||||
validationRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
marginLeft: 2,
|
||||
},
|
||||
validationText: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter_400Regular",
|
||||
},
|
||||
validateBtn: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
},
|
||||
validateText: {
|
||||
fontSize: 14,
|
||||
fontFamily: "Inter_600SemiBold",
|
||||
},
|
||||
saveBtn: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
},
|
||||
saveText: {
|
||||
fontSize: 15,
|
||||
fontFamily: "Inter_600SemiBold",
|
||||
},
|
||||
clearBtn: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
},
|
||||
clearText: {
|
||||
fontSize: 14,
|
||||
fontFamily: "Inter_500Medium",
|
||||
},
|
||||
footer: {
|
||||
marginTop: 8,
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter_400Regular",
|
||||
textAlign: "center",
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user