feat: restaurant cover photo upload
restaurants.cover_url existed in the schema and was already read by the public menu page, but nothing could ever write it — the update endpoint didn't accept it and there was no picker in the app. Adds coverUrl to PATCH /restaurants/:id and a cover photo picker in the mobile account screen, next to the existing logo picker. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
df250c40e8
commit
d33a38de4e
@@ -16,6 +16,7 @@ const createRestaurantSchema = z.object({
|
||||
const updateRestaurantSchema = z.object({
|
||||
name: z.string().min(1).max(120).optional(),
|
||||
logoUrl: z.string().nullish(),
|
||||
coverUrl: z.string().nullish(),
|
||||
phone: z.string().max(30).nullish(),
|
||||
address: z.string().max(500).nullish(),
|
||||
slug: z.string().min(2).max(100).optional(),
|
||||
@@ -108,7 +109,7 @@ export const restaurantsRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.code(400).send({ message: "invalid body", issues: parsed.error.issues });
|
||||
}
|
||||
|
||||
const { name, logoUrl, phone, address, slug } = parsed.data;
|
||||
const { name, logoUrl, coverUrl, phone, address, slug } = parsed.data;
|
||||
|
||||
let formattedSlug: string | undefined = undefined;
|
||||
if (slug !== undefined) {
|
||||
@@ -145,6 +146,7 @@ export const restaurantsRoutes: FastifyPluginAsync = async (app) => {
|
||||
.update({
|
||||
...(name !== undefined ? { name } : {}),
|
||||
...(logoUrl !== undefined ? { logo_url: logoUrl } : {}),
|
||||
...(coverUrl !== undefined ? { cover_url: coverUrl } : {}),
|
||||
...(phone !== undefined ? { phone } : {}),
|
||||
...(address !== undefined ? { address } : {}),
|
||||
...(formattedSlug !== undefined ? { slug: formattedSlug } : {}),
|
||||
|
||||
@@ -26,6 +26,7 @@ interface RestaurantDetail {
|
||||
name: string;
|
||||
slug: string;
|
||||
logo_url: string | null;
|
||||
cover_url: string | null;
|
||||
phone: string | null;
|
||||
address: string | null;
|
||||
enabled_languages?: string[];
|
||||
@@ -88,6 +89,7 @@ export default function AccountScreen() {
|
||||
const [phone, setPhone] = useState("");
|
||||
const [address, setAddress] = useState("");
|
||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||
const [coverUrl, setCoverUrl] = useState<string | null>(null);
|
||||
const [slug, setSlug] = useState("");
|
||||
const [savingRest, setSavingRest] = useState(false);
|
||||
const [deletingRestaurant, setDeletingRestaurant] = useState(false);
|
||||
@@ -130,6 +132,7 @@ export default function AccountScreen() {
|
||||
setPhone(rest.phone ?? "");
|
||||
setAddress(rest.address ?? "");
|
||||
setLogoUrl(rest.logo_url ?? null);
|
||||
setCoverUrl(rest.cover_url ?? null);
|
||||
setSlug(rest.slug);
|
||||
setEnabledLanguages(rest.enabled_languages ?? ["tr"]);
|
||||
|
||||
@@ -183,6 +186,29 @@ export default function AccountScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePickCover() {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
||||
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (!permission.granted) {
|
||||
Alert.alert("İzin Gerekli", "Kapak fotoğrafı seçebilmek için fotoğraf galerisi izni gereklidir.");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ["images"],
|
||||
allowsEditing: true,
|
||||
aspect: [16, 9],
|
||||
quality: 0.6,
|
||||
base64: true,
|
||||
});
|
||||
|
||||
if (!result.canceled && result.assets[0]) {
|
||||
const asset = result.assets[0];
|
||||
const newCover = asset.base64 ? `data:image/jpeg;base64,${asset.base64}` : asset.uri;
|
||||
setCoverUrl(newCover);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveRestaurant() {
|
||||
if (!active || !name.trim()) return;
|
||||
setSavingRest(true);
|
||||
@@ -193,6 +219,7 @@ export default function AccountScreen() {
|
||||
phone: phone.trim() || null,
|
||||
address: address.trim() || null,
|
||||
logoUrl: logoUrl || null,
|
||||
coverUrl: coverUrl || null,
|
||||
slug: slug.trim() || undefined,
|
||||
});
|
||||
|
||||
@@ -1052,6 +1079,53 @@ export default function AccountScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Cover Photo Picker */}
|
||||
<View>
|
||||
<Text style={{ fontSize: 12, fontWeight: "700", color: "#78716C", marginBottom: 6 }}>
|
||||
Kapak Fotoğrafı (Menü Üst Görseli)
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={handlePickCover}
|
||||
style={{
|
||||
width: "100%",
|
||||
aspectRatio: 16 / 9,
|
||||
borderRadius: 14,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#1C1917",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#D6D3D1",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{coverUrl ? (
|
||||
<Image source={{ uri: coverUrl }} style={{ width: "100%", height: "100%" }} resizeMode="cover" />
|
||||
) : (
|
||||
<View style={{ alignItems: "center", gap: 6 }}>
|
||||
<Ionicons name="image-outline" size={28} color="#C8A96B" />
|
||||
<Text style={{ color: "#C8A96B", fontSize: 12, fontWeight: "700" }}>
|
||||
Kapak Fotoğrafı Ekle
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
alignItems: "center",
|
||||
paddingVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 11, fontWeight: "700" }}>
|
||||
📷 {coverUrl ? "Değiştir" : "Fotoğraf Seç"}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Restaurant Name */}
|
||||
<View>
|
||||
<Text style={{ fontSize: 12, fontWeight: "700", color: "#78716C", marginBottom: 6 }}>
|
||||
|
||||
Reference in New Issue
Block a user