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({
|
const updateRestaurantSchema = z.object({
|
||||||
name: z.string().min(1).max(120).optional(),
|
name: z.string().min(1).max(120).optional(),
|
||||||
logoUrl: z.string().nullish(),
|
logoUrl: z.string().nullish(),
|
||||||
|
coverUrl: z.string().nullish(),
|
||||||
phone: z.string().max(30).nullish(),
|
phone: z.string().max(30).nullish(),
|
||||||
address: z.string().max(500).nullish(),
|
address: z.string().max(500).nullish(),
|
||||||
slug: z.string().min(2).max(100).optional(),
|
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 });
|
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;
|
let formattedSlug: string | undefined = undefined;
|
||||||
if (slug !== undefined) {
|
if (slug !== undefined) {
|
||||||
@@ -145,6 +146,7 @@ export const restaurantsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
.update({
|
.update({
|
||||||
...(name !== undefined ? { name } : {}),
|
...(name !== undefined ? { name } : {}),
|
||||||
...(logoUrl !== undefined ? { logo_url: logoUrl } : {}),
|
...(logoUrl !== undefined ? { logo_url: logoUrl } : {}),
|
||||||
|
...(coverUrl !== undefined ? { cover_url: coverUrl } : {}),
|
||||||
...(phone !== undefined ? { phone } : {}),
|
...(phone !== undefined ? { phone } : {}),
|
||||||
...(address !== undefined ? { address } : {}),
|
...(address !== undefined ? { address } : {}),
|
||||||
...(formattedSlug !== undefined ? { slug: formattedSlug } : {}),
|
...(formattedSlug !== undefined ? { slug: formattedSlug } : {}),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface RestaurantDetail {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
logo_url: string | null;
|
logo_url: string | null;
|
||||||
|
cover_url: string | null;
|
||||||
phone: string | null;
|
phone: string | null;
|
||||||
address: string | null;
|
address: string | null;
|
||||||
enabled_languages?: string[];
|
enabled_languages?: string[];
|
||||||
@@ -88,6 +89,7 @@ export default function AccountScreen() {
|
|||||||
const [phone, setPhone] = useState("");
|
const [phone, setPhone] = useState("");
|
||||||
const [address, setAddress] = useState("");
|
const [address, setAddress] = useState("");
|
||||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||||
|
const [coverUrl, setCoverUrl] = useState<string | null>(null);
|
||||||
const [slug, setSlug] = useState("");
|
const [slug, setSlug] = useState("");
|
||||||
const [savingRest, setSavingRest] = useState(false);
|
const [savingRest, setSavingRest] = useState(false);
|
||||||
const [deletingRestaurant, setDeletingRestaurant] = useState(false);
|
const [deletingRestaurant, setDeletingRestaurant] = useState(false);
|
||||||
@@ -130,6 +132,7 @@ export default function AccountScreen() {
|
|||||||
setPhone(rest.phone ?? "");
|
setPhone(rest.phone ?? "");
|
||||||
setAddress(rest.address ?? "");
|
setAddress(rest.address ?? "");
|
||||||
setLogoUrl(rest.logo_url ?? null);
|
setLogoUrl(rest.logo_url ?? null);
|
||||||
|
setCoverUrl(rest.cover_url ?? null);
|
||||||
setSlug(rest.slug);
|
setSlug(rest.slug);
|
||||||
setEnabledLanguages(rest.enabled_languages ?? ["tr"]);
|
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() {
|
async function handleSaveRestaurant() {
|
||||||
if (!active || !name.trim()) return;
|
if (!active || !name.trim()) return;
|
||||||
setSavingRest(true);
|
setSavingRest(true);
|
||||||
@@ -193,6 +219,7 @@ export default function AccountScreen() {
|
|||||||
phone: phone.trim() || null,
|
phone: phone.trim() || null,
|
||||||
address: address.trim() || null,
|
address: address.trim() || null,
|
||||||
logoUrl: logoUrl || null,
|
logoUrl: logoUrl || null,
|
||||||
|
coverUrl: coverUrl || null,
|
||||||
slug: slug.trim() || undefined,
|
slug: slug.trim() || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1052,6 +1079,53 @@ export default function AccountScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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 */}
|
{/* Restaurant Name */}
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ fontSize: 12, fontWeight: "700", color: "#78716C", marginBottom: 6 }}>
|
<Text style={{ fontSize: 12, fontWeight: "700", color: "#78716C", marginBottom: 6 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user