Déposer un Dossier de Vente
; import { Button } from
; import { Textarea } from
; import AddressAutocomplete from
; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from
; import { motion } from
; import { Upload, X, FileText, Image, Video, ArrowLeft, CheckCircle, Loader2 } from
; import { useToast } from
; import { supabase } from
; import { trackEvent } from
; type FileWithPreview = { file: File; id: string; type:
; }; const getFileType = (file: File):
=> { if (file.type.startsWith(
; if (file.type.startsWith(
; }; const Deposer = () => { const { toast } = useToast(); const [files, setFiles] = useState<FileWithPreview[]>([]); const [isSubmitted, setIsSubmitted] = useState(false); const [sending, setSending] = useState(false); const [formData, setFormData] = useState({ nom:
, description: `La taille totale des pièces jointes (${(totalSize / 1024 / 1024).toFixed(1)} Mo) dépasse la limite de 4 Mo. Réduisez le nombre ou la taille des fichiers.`, variant:
, }); setSending(false); return; } // Convert files to base64 for email attachments const attachments = await Promise.all( files.map(async (f) => { const buffer = await f.file.arrayBuffer(); const base64 = btoa( new Uint8Array(buffer).reduce((data, byte) => data + String.fromCharCode(byte), '') ); return { filename: f.file.name, content: base64, }; }) ); const { data, error } = await supabase.functions.invoke('send-email', { body: { formType: 'deposer', data: formData, attachments }, }); if (error) throw error; trackEvent(
}); setIsSubmitted(true); toast({ title:
Notre équipe vous contactera sous 48h.
Une erreur est survenue. Veuillez réessayer.
> Notre équipe vous contacte sous 24h. </p> <Link to=
> <Button variant=
> <SEOHead title=
Déposez votre dossier de vente immobilière. Réponse en 48h, rachat immobilier rapide à Paris, Nice, Marseille et Île-de-France. Confidentiel et sans engagement.
> <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} > <Link to=
inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors mb-8 text-sm
> <Label htmlFor=
>Nom *</Label> <Input id=
required value={formData.nom} onChange={(e) => updateField(
, e.target.value)} placeholder=
>Prénom *</Label> <Input id=
required value={formData.prenom} onChange={(e) => updateField(
>Email *</Label> <Input id=
required value={formData.email} onChange={(e) => updateField(
>Téléphone *</Label> <Input id=
required value={formData.telephone} onChange={(e) => updateField(
>Adresse du bien *</Label> <AddressAutocomplete id=
required value={formData.adresse} onChange={(v) => updateField(
, v)} onSelect={(s) => { updateField(
, s.postcode); updateField(
, s.city); }} placeholder=
>Code postal *</Label> <Input id=
required value={formData.codePostal} onChange={(e) => updateField(
>Ville *</Label> <Input id=
required value={formData.ville} onChange={(e) => updateField(
>Surface (m²)</Label> <Input id=
value={formData.surface} onChange={(e) => updateField(
>Nombre de pièces</Label> <Input id=
value={formData.pieces} onChange={(e) => updateField(
> <Label>Situation</Label> <Select value={formData.situation} onValueChange={(v) => updateField(
, v)}> <SelectTrigger> <SelectValue placeholder=
>Succession / Indivision</SelectItem> <SelectItem value=
>Gros travaux nécessaires</SelectItem> <SelectItem value=
>Prêt relais</SelectItem> <SelectItem value=
>Mutation professionnelle</SelectItem> <SelectItem value=
>Bien invendu</SelectItem> <SelectItem value=
>Description / Commentaires</Label> <Textarea id=
rows={4} value={formData.description} onChange={(e) => updateField(
>Photos, vidéos, PDF, DPE, plans…</span> <input type=
accept=
> {f.type ===
w-5 h-5 text-accent flex-shrink-0
text-sm text-foreground truncate flex-1
text-xs text-muted-foreground
> En soumettant ce formulaire, vous acceptez que LevelStone collecte vos données afin de vous recontacter dans le cadre de votre projet immobilier. Pour en savoir plus, consultez notre{
} <Link to=
text-accent hover:underline
✅ Demande envoyée avec succès
Notre équipe vous contacte sous 24h.
Remplissez ce formulaire et joignez vos documents. Nous vous répondons sous 48h.
Description / Commentaires
Joignez vos photos, vidéos, DPE, plans ou tout document utile à l'évaluation.
Cliquez pour ajouter des fichiers
Photos, vidéos, PDF, DPE, plans…
Politique de confidentialité
; import { cva, type VariantProps } from
; const buttonVariants = cva(
, { variants: { variant: { default:
bg-destructive text-destructive-foreground hover:bg-destructive/90
border border-input bg-background hover:bg-accent hover:text-accent-foreground
bg-secondary text-secondary-foreground hover:bg-secondary/80
text-primary underline-offset-4 hover:underline
bg-primary text-primary-foreground font-semibold shadow-[0_8px_30px_-8px_hsl(232_100%_62%/0.5)] hover:bg-primary/90 hover:-translate-y-0.5 hover:shadow-[0_12px_40px_-8px_hsl(232_100%_62%/0.6)]
border border-foreground/15 text-foreground bg-background/60 backdrop-blur-md hover:bg-background hover:border-foreground/30
bg-foreground text-background font-semibold hover:bg-primary hover:text-primary-foreground hover:-translate-y-0.5
bg-primary text-primary-foreground font-bold shadow-lg hover:bg-primary/90 hover:-translate-y-0.5 text-base
, }, }, defaultVariants: { variant:
, }, }, ); export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean; } const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot :
; const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<
, className, )} ref={ref} {...props} /> ); }, ); Input.displayName =
, className, )} ref={ref} {...props} /> ); }); Textarea.displayName =
; type Suggestion = { label: string; context: string; postcode: string; city: string; }; interface AddressAutocompleteProps { value: string; onChange: (value: string) => void; onSelect?: (suggestion: Suggestion) => void; placeholder?: string; className?: string; id?: string; required?: boolean; onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void; } const AddressAutocomplete = ({ value, onChange, onSelect, placeholder =
, city: f.properties.city ||
, })); setSuggestions(results); setOpen(results.length > 0); setActiveIndex(-1); } catch { setSuggestions([]); } }, []); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; onChange(val); clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => fetchSuggestions(val), 300); }; const selectSuggestion = (s: Suggestion) => { onChange(s.label); onSelect?.(s); setOpen(false); setSuggestions([]); }; const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (open && suggestions.length > 0) { if (e.key ===
) { e.preventDefault(); setActiveIndex((i) => (i < suggestions.length - 1 ? i + 1 : 0)); return; } if (e.key ===
) { e.preventDefault(); setActiveIndex((i) => (i > 0 ? i - 1 : suggestions.length - 1)); return; } if (e.key ===
&& activeIndex >= 0) { e.preventDefault(); selectSuggestion(suggestions[activeIndex]); return; } if (e.key ===
) { setOpen(false); return; } } onKeyDown?.(e); }; // Close on click outside useEffect(() => { const handler = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener(
, handler); return () => document.removeEventListener(
> <input id={id} type=
required={required} value={value} onChange={handleChange} onKeyDown={handleKeyDown} onFocus={() => suggestions.length > 0 && setOpen(true)} placeholder={placeholder} autoComplete=
, i === activeIndex ?
text-foreground hover:bg-accent/5
; import * as LabelPrimitive from
; const labelVariants = cva(
; import * as SelectPrimitive from
; import { Check, ChevronDown, ChevronUp } from
data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1
, position ===
h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]
py-1.5 pl-8 pr-2 text-sm font-semibold
relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground
absolute left-2 flex h-3.5 w-3.5 items-center justify-center
; import { Menu, X } from
; import { Link, useLocation } from
; const navLinks = [ { label:
}, ]; const Navbar = () => { const [open, setOpen] = useState(false); const [scrolled, setScrolled] = useState(false); const location = useLocation(); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 20); onScroll(); window.addEventListener(
, onScroll, { passive: true }); return () => window.removeEventListener(
, onScroll); }, []); const handleNavClick = (href: string) => { setOpen(false); if (href.startsWith(
)) { if (location.pathname !==
) { window.location.href =
+ href; return; } document.getElementById(href.slice(1))?.scrollIntoView({ behavior:
}); } }; const scrollToForm = () => { setOpen(false); if (location.pathname !==
; return; } document.getElementById(
)?.scrollIntoView({ behavior:
bg-background/60 backdrop-blur-md
container mx-auto px-4 sm:px-6 lg:px-8
flex items-center justify-between h-20
> {navLinks.map((link) => link.href.startsWith(
> {link.label} </Link> ) )} <Button variant=
aria-label=
link.href.startsWith("#") ? (
Proposer un bien
absolute inset-0 noise-overlay pointer-events-none
container mx-auto px-4 sm:px-6 lg:px-8 relative
inline-flex items-center gap-1 mb-6
font-display text-3xl font-bold tracking-tight text-accent
font-display text-3xl font-bold tracking-tight text-white
font-display text-2xl sm:text-3xl md:text-4xl font-bold text-white/90 tracking-tight leading-[1.1] text-balance
italic font-serif font-normal text-white/40
grid grid-cols-2 md:grid-cols-4 gap-8 pb-12 border-b border-white/10
text-[11px] font-bold text-white/40 mb-4 uppercase tracking-[0.2em]
> <li><Link to=
text-sm text-white/70 hover:text-accent transition-colors
> <li> <a href=
text-sm text-white/70 hover:text-accent transition-colors flex items-center gap-2
mt-10 flex flex-col md:flex-row items-center justify-between gap-4 text-xs text-white/30
Achat immobilier direct.
Paris · Île-de-France · Nice · Marseille.
Paris · IDF · Nice · Marseille
; interface SEOHeadProps { title: string; description: string; canonical?: string; jsonLd?: object; faqSchema?: object; ogType?: string; ogImage?: string; } const DEFAULT_OG_IMAGE =
; const SEOHead = ({ title, description, canonical, jsonLd, faqSchema, ogType =
, ogImage }: SEOHeadProps) => { useEffect(() => { document.title = title; const setMeta = (name: string, content: string, attr =
) => { let el = document.querySelector(`meta[${attr}=
]`) as HTMLMetaElement | null; if (!el) { el = document.createElement(
); el.setAttribute(attr, name); document.head.appendChild(el); } el.content = content; }; setMeta(
, description); setMeta(
, ogImage || DEFAULT_OG_IMAGE,
); if (canonical) { let link = document.querySelector('link[rel=
]') as HTMLLinkElement | null; if (!link) { link = document.createElement(
; document.head.appendChild(link); } link.href = canonical; } // Always inject Organization JSON-LD let orgScript = document.getElementById(
) as HTMLScriptElement | null; if (!orgScript) { orgScript = document.createElement(
; orgScript.type =
; document.head.appendChild(orgScript); } orgScript.textContent = JSON.stringify({
) as HTMLScriptElement | null; if (!script) { script = document.createElement(
; document.head.appendChild(script); } script.textContent = JSON.stringify(jsonLd); } if (faqSchema) { let faqEl = document.getElementById(
) as HTMLScriptElement | null; if (!faqEl) { faqEl = document.createElement(
; document.head.appendChild(faqEl); } faqEl.textContent = JSON.stringify(faqSchema); } return () => { const el = document.getElementById(
); if (el) el.remove(); const faq = document.getElementById(