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:
AyrisAI
2026-08-29 01:58:10 +03:00
co-authored by Claude Sonnet 5
parent df250c40e8
commit d33a38de4e
2 changed files with 77 additions and 1 deletions
+3 -1
View File
@@ -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 } : {}),
+74
View File
@@ -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 }}>