2026-07-12 12:41:35 +00:00
<!DOCTYPE html>
< html lang = "sv" >
< head >
< meta charset = "UTF-8" >
< link rel = "icon" type = "image/x-icon" href = "/favicon.ico" >
< link rel = "icon" type = "image/svg+xml" href = "/favicon.svg" >
< link rel = "icon" type = "image/png" sizes = "32x32" href = "/favicon-32x32.png" >
< link rel = "apple-touch-icon" sizes = "180x180" href = "/apple-touch-icon.png" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, viewport-fit=cover" >
< meta name = "apple-mobile-web-app-capable" content = "yes" >
< meta name = "apple-mobile-web-app-status-bar-style" content = "default" >
< meta name = "apple-mobile-web-app-title" content = "LandveX" >
< title > LandveX — Fastighetsplattform< / title >
< style >
: root {
--blue : #007AFF ;
--green : #34C759 ;
--red : #FF3B30 ;
--orange : #FF9500 ;
--purple : #AF52DE ;
--gray : #8E8E93 ;
--gray2 : #AEAEB2 ;
--gray3 : #C7C7CC ;
--gray4 : #D1D1D6 ;
--gray5 : #E5E5EA ;
--gray6 : #F2F2F7 ;
--label : #000000 ;
--label2 : rgba ( 60 , 60 , 67 , 0.8 ) ;
--label3 : rgba ( 60 , 60 , 67 , 0.6 ) ;
--sep : rgba ( 60 , 60 , 67 , 0.16 ) ;
--card-bg : #FFFFFF ;
--bg : #F2F2F7 ;
--safe-bottom : env ( safe - area - inset - bottom , 0 px ) ;
--safe-top : env ( safe - area - inset - top , 0 px ) ;
}
* { box-sizing : border-box ; margin : 0 ; padding : 0 ; -webkit- tap-highlight-color : transparent ; }
html , body { height : 100 % ; background : var ( - - bg ) ; font-family : - apple-system , "SF Pro Display" , BlinkMacSystemFont , "Helvetica Neue" , sans-serif ; color : var ( - - label ) ; }
. navbar {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
padding-top : var ( - - safe - top ) ;
background : rgba ( 242 , 242 , 247 , 0.9 ) ;
backdrop-filter : saturate ( 180 % ) blur ( 20 px ) ;
-webkit- backdrop-filter : saturate ( 180 % ) blur ( 20 px ) ;
border-bottom : 0.5 px solid var ( - - sep ) ;
}
. navbar-inner { display : flex ; align-items : center ; justify-content : space-between ; height : 44 px ; padding : 0 16 px ; }
. navbar-logo { display : flex ; align-items : center ; gap : 8 px ; }
. navbar-logo span { font-size : 17 px ; font-weight : 700 ; color : var ( - - blue ) ; letter-spacing : -0.5 px ; }
. navbar-badge { background : var ( - - blue ) ; color : #1d1d1f ; font-size : 11 px ; font-weight : 600 ; padding : 2 px 7 px ; border-radius : var ( - - radius - md ) ; }
. scroll-area {
position : fixed ; top : 0 ; bottom : 0 ; left : 0 ; right : 0 ;
padding-top : calc ( 44 px + var ( - - safe - top ) + 8 px ) ;
padding-bottom : calc ( 83 px + var ( - - safe - bottom ) ) ;
overflow-y : auto ; overflow-x : hidden ;
-webkit- overflow-scrolling : touch ;
}
. tabbar {
position : fixed ; bottom : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
padding-bottom : var ( - - safe - bottom ) ;
background : rgba ( 242 , 242 , 247 , 0.9 ) ;
backdrop-filter : saturate ( 180 % ) blur ( 20 px ) ;
-webkit- backdrop-filter : saturate ( 180 % ) blur ( 20 px ) ;
border-top : 0.5 px solid var ( - - sep ) ;
display : flex ; height : calc ( 49 px + var ( - - safe - bottom ) ) ;
}
. tab-item {
flex : 1 ; display : flex ; flex-direction : column ; align-items : center ; justify-content : center ;
gap : 3 px ; cursor : pointer ; padding : 6 px 0 ; border : none ; background : none ;
transition : opacity 0.15 s ;
}
. tab-item : active { opacity : 0.6 ; }
. tab-icon { font-size : 22 px ; line-height : 1 ; }
. tab-label { font-size : 10 px ; font-weight : 500 ; color : var ( - - gray ) ; }
. tab-item . active . tab-label { color : var ( - - blue ) ; }
. page { display : none ; padding : 0 0 8 px ; }
. page . active { display : block ; }
. page-header { padding : 8 px 16 px 4 px ; }
. large-title { font-size : 34 px ; font-weight : 700 ; letter-spacing : -0.5 px ; line-height : 1.1 ; }
. large-subtitle { font-size : 15 px ; color : var ( - - gray ) ; margin-top : 4 px ; }
. section-header {
padding : 20 px 16 px 6 px ;
font-size : 13 px ; font-weight : 600 ; color : var ( - - gray ) ;
text-transform : uppercase ; letter-spacing : 0.3 px ;
}
. inset-group { margin : 0 16 px 4 px ; border-radius : var ( - - radius - md ) ; overflow : hidden ; background : var ( - - card - bg ) ; }
. inset-group . cell { display : flex ; align-items : center ; padding : 12 px 16 px ; min-height : 44 px ; border-bottom : 0.5 px solid var ( - - sep ) ; }
. inset-group . cell : last-child { border-bottom : none ; }
. cell-label { flex : 1 ; font-size : 17 px ; }
. cell-value { font-size : 17 px ; color : var ( - - gray ) ; }
. searchbar-wrap { padding : 8 px 16 px ; }
. searchbar {
display : flex ; align-items : center ; gap : 8 px ;
background : var ( - - card - bg ) ; border-radius : var ( - - radius - md ) ;
padding : 8 px 12 px ; box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. searchbar input { flex : 1 ; border : none ; outline : none ; font-size : 17 px ; background : transparent ; color : var ( - - label ) ; font-family : inherit ; }
. searchbar input :: placeholder { color : var ( - - gray3 ) ; }
. searchbar . icon { color : var ( - - gray3 ) ; font-size : 18 px ; }
. seg-wrap { padding : 8 px 16 px ; }
. seg-control { display : flex ; background : var ( - - gray5 ) ; border-radius : var ( - - radius - md ) ; padding : 2 px ; gap : 2 px ; }
. seg-btn {
flex : 1 ; padding : 6 px 4 px ; border : none ; background : none ; border-radius : var ( - - radius - sm ) ;
font-size : 13 px ; font-weight : 500 ; color : var ( - - gray ) ; cursor : pointer ;
transition : all 0.2 s ; font-family : inherit ;
}
. seg-btn . active { background : var ( - - card - bg ) ; color : var ( - - label ) ; box-shadow : 0 1 px 4 px rgba ( 0 , 0 , 0 , 0.12 ) ; font-weight : 600 ; }
. prop-list { padding : 0 16 px ; display : flex ; flex-direction : column ; gap : 10 px ; }
. prop-card {
background : var ( - - card - bg ) ; border-radius : var ( - - radius - md ) ;
padding : 16 px ; box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ;
cursor : pointer ; transition : transform 0.15 s ;
}
. prop-card : active { transform : scale ( 0.98 ) ; }
. prop-card-top { display : flex ; align-items : flex-start ; justify-content : space-between ; gap : 8 px ; }
. prop-type-badge { font-size : 11 px ; font-weight : 600 ; padding : 3 px 8 px ; border-radius : var ( - - radius - sm ) ; white-space : nowrap ; }
. prop-address { font-size : 17 px ; font-weight : 600 ; letter-spacing : -0.3 px ; }
. prop-owner { font-size : 14 px ; color : var ( - - gray ) ; margin-top : 2 px ; }
. prop-meta { display : flex ; gap : 12 px ; margin-top : 12 px ; flex-wrap : wrap ; }
. prop-meta-item { display : flex ; flex-direction : column ; gap : 2 px ; }
. prop-meta-label { font-size : 11 px ; color : var ( - - gray ) ; font-weight : 500 ; text-transform : uppercase ; letter-spacing : 0.3 px ; }
. prop-meta-value { font-size : 15 px ; font-weight : 600 ; }
. prop-condition { display : inline-flex ; align-items : center ; gap : 4 px ; font-size : 13 px ; font-weight : 500 ; margin-top : 10 px ; }
. condition-dot { width : 8 px ; height : 8 px ; border-radius : var ( - - radius - full ) ; }
. cond-good { background : var ( - - green ) ; }
. cond-fair { background : var ( - - orange ) ; }
. cond-poor , . cond-critical { background : var ( - - red ) ; }
. cond-unknown { background : var ( - - gray3 ) ; }
. form-group { margin : 0 16 px 4 px ; border-radius : var ( - - radius - md ) ; overflow : hidden ; background : var ( - - card - bg ) ; }
. form-field { display : flex ; flex-direction : column ; padding : 10 px 16 px ; border-bottom : 0.5 px solid var ( - - sep ) ; }
. form-field : last-child { border-bottom : none ; }
. form-label { font-size : 12 px ; font-weight : 500 ; color : var ( - - gray ) ; margin-bottom : 4 px ; text-transform : uppercase ; letter-spacing : 0.3 px ; }
. form-input { font-size : 17 px ; border : none ; outline : none ; background : transparent ; color : var ( - - label ) ; font-family : inherit ; width : 100 % ; padding : 2 px 0 ; }
. form-input :: placeholder { color : var ( - - gray3 ) ; }
. form-select { font-size : 17 px ; border : none ; outline : none ; background : transparent ; color : var ( - - label ) ; font-family : inherit ; width : 100 % ; padding : 2 px 0 ; appearance : none ; -webkit- appearance : none ; }
. btn-primary {
display : block ; width : calc ( 100 % - 32 px ) ; margin : 16 px 16 px 8 px ;
background : var ( - - blue ) ; color : #1d1d1f ; font-size : 17 px ; font-weight : 600 ;
border : none ; border-radius : var ( - - radius - md ) ; padding : 14 px ; cursor : pointer ;
font-family : inherit ; transition : opacity 0.15 s ; letter-spacing : -0.3 px ;
}
. btn-primary : active { opacity : 0.75 ; }
. btn-primary : disabled { background : var ( - - gray3 ) ; cursor : not-allowed ; }
. btn-secondary {
display : block ; width : calc ( 100 % - 32 px ) ; margin : 4 px 16 px ;
background : var ( - - gray6 ) ; color : var ( - - blue ) ; font-size : 17 px ; font-weight : 500 ;
border : none ; border-radius : var ( - - radius - md ) ; padding : 14 px ; cursor : pointer ;
font-family : inherit ; transition : opacity 0.15 s ;
}
. btn-secondary : active { opacity : 0.7 ; }
. stats-grid { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 10 px ; padding : 0 16 px ; }
. stat-card { background : var ( - - card - bg ) ; border-radius : var ( - - radius - md ) ; padding : 14 px ; box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ; }
. stat-icon { font-size : 24 px ; margin-bottom : 8 px ; }
. stat-value { font-size : 22 px ; font-weight : 700 ; letter-spacing : -0.5 px ; }
. stat-label { font-size : 13 px ; color : var ( - - gray ) ; margin-top : 2 px ; }
. bar-chart { padding : 0 16 px ; }
. bar-item { margin-bottom : 12 px ; }
. bar-row { display : flex ; align-items : center ; gap : 8 px ; margin-bottom : 4 px ; }
. bar-name { font-size : 13 px ; font-weight : 500 ; flex : 0 0 120 px ; overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ; }
. bar-count { font-size : 13 px ; color : var ( - - gray ) ; margin-left : auto ; }
. bar-track { height : 8 px ; background : var ( - - gray5 ) ; border-radius : var ( - - radius - sm ) ; overflow : hidden ; }
. bar-fill { height : 100 % ; border-radius : var ( - - radius - sm ) ; background : var ( - - blue ) ; transition : width 0.6 s cubic-bezier ( 0.34 , 1.56 , 0.64 , 1 ) ; }
. trend-row { display : flex ; align-items : center ; padding : 12 px 16 px ; border-bottom : 0.5 px solid var ( - - sep ) ; }
. trend-row : last-child { border-bottom : none ; }
. trend-label { flex : 1 ; font-size : 16 px ; }
. trend-val { font-size : 16 px ; font-weight : 600 ; }
. trend-pos { color : var ( - - green ) ; }
. trend-neg { color : var ( - - red ) ; }
. trend-neutral { color : var ( - - label ) ; }
. toast {
position : fixed ; bottom : calc ( 83 px + var ( - - safe - bottom ) + 12 px ) ; left : 50 % ; transform : translateX ( -50 % ) translateY ( 20 px ) ;
background : rgba ( 50 , 50 , 50 , 0.92 ) ; color : #1d1d1f ; padding : 10 px 18 px ; border-radius : var ( - - radius - xl ) ;
font-size : 15 px ; font-weight : 500 ; opacity : 0 ; transition : all 0.3 s ; pointer-events : none ;
white-space : nowrap ; backdrop-filter : blur ( 10 px ) ; z-index : 200 ;
}
. toast . show { opacity : 1 ; transform : translateX ( -50 % ) translateY ( 0 ) ; }
. spinner { display : inline-block ; width : 20 px ; height : 20 px ; border : 2 px solid var ( - - gray4 ) ; border-top-color : var ( - - blue ) ; border-radius : var ( - - radius - full ) ; animation : spin 0.7 s linear infinite ; }
@ keyframes spin { to { transform : rotate ( 360 deg ) ; } }
. loading-center { display : flex ; justify-content : center ; align-items : center ; padding : 40 px ; gap : 10 px ; color : var ( - - gray ) ; font-size : 15 px ; }
. empty-state { text-align : center ; padding : 48 px 32 px ; }
. empty-icon { font-size : 48 px ; margin-bottom : 16 px ; }
. empty-title { font-size : 20 px ; font-weight : 600 ; margin-bottom : 8 px ; }
. empty-body { font-size : 15 px ; color : var ( - - gray ) ; line-height : 1.5 ; }
. new-badge { background : rgba ( 52 , 199 , 89 , 0.15 ) ; color : var ( - - green ) ; font-size : 11 px ; font-weight : 700 ; padding : 2 px 7 px ; border-radius : var ( - - radius - sm ) ; }
@ media ( max-width : 640px ) {
. hero h1 { font-size : 2 rem !important ; }
. hero p { font-size : 1 rem !important ; }
. card { padding : 1.5 rem !important ; }
. grid { grid-template-columns : 1 fr !important ; }
. btn { padding : 0.75 rem 1.5 rem !important ; font-size : 1 rem !important ; }
}
< / style >
< style id = "wcag-contrast-enforcement" >
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
/* Regel: Ingen text under 70% opacity. Aldrig. */
body { -webkit- font-smoothing : antialiased ; -moz- osx-font-smoothing : grayscale ; }
/* Fix common low-contrast patterns */
[ style * = "rgba(60,60,67,0.4)" ] { color : rgba ( 60 , 60 , 67 , 0.65 ) !important ; }
[ style * = "rgba(60,60,67,0.3)" ] { color : rgba ( 60 , 60 , 67 , 0.65 ) !important ; }
[ style * = "rgba(0,0,0,0.4)" ] { color : rgba ( 0 , 0 , 0 , 0.72 ) !important ; }
[ style * = "rgba(0,0,0,0.3)" ] { color : rgba ( 0 , 0 , 0 , 0.72 ) !important ; }
[ style * = "opacity: 0.4" ] { opacity : 0.72 !important ; }
[ style * = "opacity: 0.3" ] { opacity : 0.72 !important ; }
@ media ( max-width : 640px ) {
. hero h1 { font-size : 2 rem !important ; }
. hero p { font-size : 1 rem !important ; }
. card { padding : 1.5 rem !important ; }
. grid { grid-template-columns : 1 fr !important ; }
. btn { padding : 0.75 rem 1.5 rem !important ; font-size : 1 rem !important ; }
}
< / style >
< script type = "application/ld+json" >
{
"@context" : "https://schema.org" ,
"@type" : [
"Organization" ,
"Corporation"
] ,
"@id" : "https://landvex.com/#organization" ,
"name" : "Landvex" ,
"alternateName" : [
"LandveX" ,
"Landvex AB" ,
"LandveX AB"
] ,
"url" : "https://landvex.com" ,
"logo" : {
"@type" : "ImageObject" ,
"url" : "https://landvex.com/apple-touch-icon.png" ,
"width" : 180 ,
"height" : 180
} ,
"foundingDate" : "2024" ,
"foundingLocation" : {
"@type" : "Place" ,
2026-07-14 15:27:33 +00:00
"addressLocality" : "Stockholm" ,
2026-07-12 12:41:35 +00:00
"addressRegion" : "Stockholm" ,
"addressCountry" : "SE"
} ,
"legalName" : "Landvex Inc" ,
"address" : {
"@type" : "PostalAddress" ,
"addressLocality" : "Houston" ,
"addressRegion" : "TX" ,
"addressCountry" : "US"
} ,
"location" : [
{
"@type" : "Place" ,
"name" : "Houston, Texas, USA (US HQ)" ,
"address" : {
"@type" : "PostalAddress" ,
"addressLocality" : "Houston" ,
"addressRegion" : "TX" ,
"addressCountry" : "US"
}
} ,
{
"@type" : "Place" ,
2026-07-14 15:27:33 +00:00
"name" : "Stockholm, Sweden (EU HQ)" ,
2026-07-12 12:41:35 +00:00
"address" : {
"@type" : "PostalAddress" ,
2026-07-14 15:27:33 +00:00
"addressLocality" : "Stockholm" ,
2026-07-12 12:41:35 +00:00
"addressRegion" : "Stockholm" ,
"addressCountry" : "SE"
}
}
] ,
"taxID" : "pending-EIN" ,
"description" : "Landvex is a decision intelligence company that identifies contradictions between official narratives and observed physical reality. Using the quiXzoom field observation network and the AMOS AI analysis engine, Landvex delivers infrastructure risk indexes, urban intelligence scores, and contradiction reports to infrastructure owners, municipalities, enterprises, and investors globally." ,
2026-07-14 15:27:33 +00:00
"disambiguatingDescription" : "Landvex (LandveX) is a decision intelligence company with US headquarters in Houston, Texas (Landvex Inc.) and European headquarters in Stockholm, Sweden (Landvex AB, org.nr 559141-7042). Landvex delivers control intelligence — infrastructure risk scores, urban intelligence, and contradiction analysis — to municipalities, enterprises, infrastructure operators and investors. Landvex is not a UK meat wholesaler, food distributor, logistics company, package tracker, or land investment platform." ,
2026-07-12 12:41:35 +00:00
"knowsAbout" : [
"control intelligence" ,
"decision intelligence" ,
"infrastructure risk assessment" ,
"urban intelligence" ,
"field data collection" ,
"contradiction detection" ,
"physical world analytics" ,
"geospatial analytics"
] ,
"brand" : {
"@type" : "Brand" ,
"name" : "Landvex" ,
"slogan" : "Control intelligence for the physical world."
} ,
"sameAs" : [
"https://landvex.com" ,
"https://www.linkedin.com/company/landvex" ,
"https://x.com/landvex" ,
"https://twitter.com/landvex" ,
"https://www.crunchbase.com/organization/landvex" ,
"https://github.com/landvex" ,
"https://www.instagram.com/landvex"
] ,
"contactPoint" : {
"@type" : "ContactPoint" ,
"email" : "contact@landvex.com" ,
"contactType" : "sales"
} ,
"slogan" : "Where reported reality conflicts with observed reality."
}
< / script >
< / head >
< body >
< nav class = "navbar" >
< div class = "navbar-inner" >
< div class = "navbar-logo" >
< svg width = "28" height = "28" viewBox = "0 0 28 28" fill = "none" >
< rect width = "28" height = "28" rx = "7" fill = "#007AFF" / >
< path d = "M6 21L10 10L14 16L18 8L22 21H6Z" fill = "white" stroke = "white" stroke-width = "1.5" stroke-linejoin = "round" / >
< / svg >
< span > LandveX< / span >
< / div >
< span class = "navbar-badge" > LIVE< / span >
< / div >
< / nav >
< div class = "scroll-area" id = "scrollArea" >
<!-- TAB 1: SEARCH -->
< div class = "page active" id = "page-search" >
< div class = "page-header" >
< div class = "large-title" > Fastigheter< / div >
< div class = "large-subtitle" > Sök och utforska fastighetsregistret< / div >
< / div >
< div class = "searchbar-wrap" >
< div class = "searchbar" >
< span class = "icon" > < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" > < circle cx = "7" cy = "7" r = "5" stroke = "#666" stroke-width = "2" / > < path d = "M11 11L14 14" stroke = "#666" stroke-width = "2" stroke-linecap = "round" / > < / svg > < / span >
< input type = "text" id = "cityInput" placeholder = "Stad (t.ex. Stockholm)" autocomplete = "off" autocorrect = "off" >
< / div >
< / div >
< div class = "seg-wrap" >
< div class = "seg-control" id = "typeSegment" >
< button class = "seg-btn active" data-type = "" > Alla< / button >
< button class = "seg-btn" data-type = "commercial" > Kommersiell< / button >
< button class = "seg-btn" data-type = "residential" > Bostad< / button >
< button class = "seg-btn" data-type = "industrial" > Industri< / button >
< / div >
< / div >
< div class = "section-header" id = "searchResultHeader" > Alla fastigheter< / div >
< div id = "propList" class = "prop-list" >
< div class = "loading-center" > < div class = "spinner" > < / div > Laddar...< / div >
< / div >
< / div >
<!-- TAB 2: REGISTER -->
< div class = "page" id = "page-register" >
< div class = "page-header" >
< div class = "large-title" > Registrera< / div >
< div class = "large-subtitle" > Lägg till ny fastighet i registret< / div >
< / div >
< div class = "section-header" > Fastighetsuppgifter< / div >
< div class = "form-group" >
< div class = "form-field" >
< label class = "form-label" > Adress *< / label >
< input class = "form-input" id = "regAddress" type = "text" placeholder = "t.ex. Storgatan 12" >
< / div >
< div class = "form-field" >
< label class = "form-label" > Fastighetstyp *< / label >
< select class = "form-select" id = "regType" >
< option value = "" > Välj typ< / option >
< option value = "Kontorsfastighet" > Kontorsfastighet< / option >
< option value = "Handelsfastighet" > Handelsfastighet< / option >
< option value = "Bostadsfastighet" > Bostadsfastighet< / option >
< option value = "Industrifastighet" > Industrifastighet< / option >
< option value = "Blandad fastighet" > Blandad fastighet< / option >
< option value = "Lager" > Lager< / option >
< / select >
< / div >
< div class = "form-field" >
< label class = "form-label" > Yta (kvm) *< / label >
< input class = "form-input" id = "regSize" type = "number" placeholder = "t.ex. 2500" >
< / div >
< div class = "form-field" >
< label class = "form-label" > Ägare *< / label >
< input class = "form-input" id = "regOwner" type = "text" placeholder = "t.ex. AB Fastighet" >
< / div >
< / div >
< div class = "section-header" > Plats & Detaljer< / div >
< div class = "form-group" >
< div class = "form-field" >
< label class = "form-label" > Stad< / label >
< select class = "form-select" id = "regCity" >
< option value = "Stockholm" > Stockholm< / option >
< option value = "Göteborg" > Göteborg< / option >
< option value = "Malmö" > Malmö< / option >
< option value = "Uppsala" > Uppsala< / option >
< option value = "Linköping" > Linköping< / option >
< option value = "Annan" > Annan< / option >
< / select >
< / div >
< div class = "form-field" >
< label class = "form-label" > Byggnadsår< / label >
< input class = "form-input" id = "regYear" type = "number" placeholder = "t.ex. 2005" >
< / div >
< div class = "form-field" >
< label class = "form-label" > Antal våningar< / label >
< input class = "form-input" id = "regFloors" type = "number" placeholder = "t.ex. 4" >
< / div >
< div class = "form-field" >
< label class = "form-label" > Taxeringsvärde (SEK)< / label >
< input class = "form-input" id = "regValue" type = "number" placeholder = "t.ex. 45000000" >
< / div >
< / div >
< button class = "btn-primary" id = "registerBtn" onclick = "registerProperty()" > Registrera fastighet< / button >
< button class = "btn-secondary" onclick = "clearForm()" > Rensa formulär< / button >
< / div >
<!-- TAB 3: MARKET -->
< div class = "page" id = "page-market" >
< div class = "page-header" >
< div class = "large-title" > Marknad< / div >
< div class = "large-subtitle" > Marknadsanalys & nyckeltal< / div >
< / div >
< div class = "seg-wrap" >
< div class = "seg-control" id = "marketCitySegment" >
< button class = "seg-btn active" data-city = "" > Sverige< / button >
< button class = "seg-btn" data-city = "Stockholm" > Stockholm< / button >
< button class = "seg-btn" data-city = "Göteborg" > Göteborg< / button >
< button class = "seg-btn" data-city = "Malmö" > Malmö< / button >
< / div >
< / div >
< div class = "section-header" > Nyckeltal< / div >
< div class = "stats-grid" id = "statsGrid" >
< div class = "loading-center" style = "grid-column:1/-1" > < div class = "spinner" > < / div > < / div >
< / div >
< div class = "section-header" > Fördelning per typ< / div >
< div class = "inset-group" id = "typeDistribution" > < / div >
< div class = "section-header" > Städer< / div >
< div class = "bar-chart" id = "cityBarChart" > < / div >
< div class = "section-header" > Marknadstrender 2026< / div >
< div class = "inset-group" >
< div class = "trend-row" > < span class = "trend-label" > Q1 2026 prisutveckling< / span > < span class = "trend-val trend-pos" id = "trendQ1" > —< / span > < / div >
< div class = "trend-row" > < span class = "trend-label" > Årsappriciering< / span > < span class = "trend-val trend-pos" id = "trendAnnual" > —< / span > < / div >
< div class = "trend-row" > < span class = "trend-label" > Transaktioner YTD< / span > < span class = "trend-val trend-neutral" id = "trendTxn" > —< / span > < / div >
< div class = "trend-row" > < span class = "trend-label" > Snitt dagar på marknaden< / span > < span class = "trend-val trend-neutral" id = "trendDays" > —< / span > < / div >
< div class = "trend-row" > < span class = "trend-label" > Vakansgrad kommersiellt< / span > < span class = "trend-val" id = "trendVacancy" > —< / span > < / div >
< div class = "trend-row" > < span class = "trend-label" > Direktavkastning (cap rate)< / span > < span class = "trend-val trend-neutral" id = "trendCapRate" > —< / span > < / div >
< / div >
< div style = "height:16px" > < / div >
< / div >
< / div >
< nav class = "tabbar" >
< button class = "tab-item active" onclick = "switchTab('search',this)" >
< span class = "tab-icon" > < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" > < circle cx = "7" cy = "7" r = "5" stroke = "#666" stroke-width = "2" / > < path d = "M11 11L14 14" stroke = "#666" stroke-width = "2" stroke-linecap = "round" / > < / svg > < / span >
< span class = "tab-label" > Sök< / span >
< / button >
< button class = "tab-item" onclick = "switchTab('register',this)" >
< span class = "tab-icon" > < / span >
< span class = "tab-label" > Registrera< / span >
< / button >
< button class = "tab-item" onclick = "switchTab('market',this)" >
< span class = "tab-icon" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "#0066FF" stroke-width = "2" > < path d = "M18 20V10" / > < path d = "M12 20V4" / > < path d = "M6 20v-6" / > < / svg > < / span >
< span class = "tab-label" > Marknad< / span >
< / button >
< / nav >
< div class = "toast" id = "toast" > < / div >
< script >
const API = '/api/landvex' ;
function demoToken ( ) {
const h = btoa ( JSON . stringify ( { alg : 'none' , typ : 'JWT' } ) ) . replace ( /=/g , '' ) ;
const p = btoa ( JSON . stringify ( { sub : 'demo@landvex.se' , email : 'demo@landvex.se' , name : 'Demo' , iat : Math . floor ( Date . now ( ) / 1000 ) } ) ) . replace ( /=/g , '' ) ;
return ` ${ h } . ${ p } .demo ` ;
}
const AUTH = { 'Authorization' : ` Bearer ${ demoToken ( ) } ` , 'Content-Type' : 'application/json' } ;
let currentCity = '' , currentType = '' , marketCity = '' ;
function switchTab ( name , btn ) {
document . querySelectorAll ( '.page' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
document . querySelectorAll ( '.tab-item' ) . forEach ( t => t . classList . remove ( 'active' ) ) ;
document . getElementById ( 'page-' + name ) . classList . add ( 'active' ) ;
btn . classList . add ( 'active' ) ;
document . getElementById ( 'scrollArea' ) . scrollTop = 0 ;
if ( name === 'market' ) loadMarket ( marketCity ) ;
}
function showToast ( msg ) {
const t = document . getElementById ( 'toast' ) ;
t . textContent = msg ; t . classList . add ( 'show' ) ;
setTimeout ( ( ) => t . classList . remove ( 'show' ) , 2800 ) ;
}
function fmtSEK ( n ) {
if ( ! n ) return '—' ;
if ( n >= 1e9 ) return ( n / 1e9 ) . toFixed ( 1 ) + ' Mdr' ;
if ( n >= 1e6 ) return ( n / 1e6 ) . toFixed ( 0 ) + ' Mkr' ;
return n . toLocaleString ( 'sv-SE' ) + ' kr' ;
}
function fmtSqm ( n ) { return n ? n . toLocaleString ( 'sv-SE' ) + ' kvm' : '—' ; }
function condClass ( c ) { return ( { good : 'cond-good' , fair : 'cond-fair' , poor : 'cond-poor' , critical : 'cond-critical' , unknown : 'cond-unknown' } ) [ c ] || 'cond-unknown' ; }
function condLabel ( c ) { return ( { good : 'Bra skick' , fair : 'Godkänt' , poor : 'Dåligt' , critical : 'Kritisk' , unknown : 'Okänt' } ) [ c ] || 'Okänt' ; }
function condColor ( c ) { return ( { good : 'var(--green)' , fair : 'var(--orange)' , poor : 'var(--red)' , critical : 'var(--red)' , unknown : 'var(--gray3)' } ) [ c ] || 'var(--gray3)' ; }
function typeColor ( t ) {
return ( { 'Kontorsfastighet' : '#007AFF' , 'Handelsfastighet' : '#FF9500' , 'Bostadsfastighet' : '#34C759' , 'Industrifastighet' : '#AF52DE' , 'Blandad fastighet' : '#FF3B30' , 'Lager' : '#8E8E93' } ) [ t ] || '#8E8E93' ;
}
function renderPropCard ( p ) {
const tc = typeColor ( p . type ) ;
return ` <div class="prop-card" onclick="showToast('ID: ${ p . property _id } ')">
<div class="prop-card-top">
<div style="flex:1">
<div class="prop-address"> ${ p . address } </div>
<div class="prop-owner"> ${ p . owner } </div>
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px">
<span class="prop-type-badge" style="background: ${ tc } 22;color: ${ tc } "> ${ p . type } </span>
${ p . registered _at ? '<span class="new-badge">NY</span>' : '' }
</div>
</div>
<div class="prop-meta">
<div class="prop-meta-item"><span class="prop-meta-label">Stad</span><span class="prop-meta-value"> ${ p . city || '—' } </span></div>
<div class="prop-meta-item"><span class="prop-meta-label">Yta</span><span class="prop-meta-value"> ${ fmtSqm ( p . area _sqm ) } </span></div>
${ p . assessed _value _sek ? ` <div class="prop-meta-item"><span class="prop-meta-label">Taxvärde</span><span class="prop-meta-value"> ${ fmtSEK ( p . assessed _value _sek ) } </span></div> ` : '' }
${ p . built _year ? ` <div class="prop-meta-item"><span class="prop-meta-label">Byggt</span><span class="prop-meta-value"> ${ p . built _year } </span></div> ` : '' }
</div>
<div class="prop-condition"><span class="condition-dot ${ condClass ( p . condition ) } "></span><span style="color: ${ condColor ( p . condition ) } "> ${ condLabel ( p . condition ) } </span></div>
</div> ` ;
}
async function loadProperties ( city , type ) {
const list = document . getElementById ( 'propList' ) ;
const header = document . getElementById ( 'searchResultHeader' ) ;
list . innerHTML = '<div class="loading-center"><div class="spinner"></div> Söker...</div>' ;
const q = new URLSearchParams ( ) ;
if ( city ) q . set ( 'city' , city ) ;
if ( type ) q . set ( 'type' , type ) ;
try {
const r = await fetch ( ` ${ API } /properties/search? ${ q } ` , { headers : AUTH } ) ;
const d = await r . json ( ) ;
const parts = [ ] ;
if ( city ) parts . push ( city ) ;
if ( type ) parts . push ( { commercial : 'Kommersiella' , residential : 'Bostäder' , industrial : 'Industrier' } [ type ] || type ) ;
header . textContent = ( parts . join ( ' · ' ) || 'Alla fastigheter' ) + ` ( ${ d . total } ) ` ;
if ( ! d . properties ? . length ) {
list . innerHTML = '<div class="empty-state"><div class="empty-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div><div class="empty-title">Inga fastigheter</div><div class="empty-body">Prova en annan stad eller kategori.</div></div>' ;
return ;
}
list . innerHTML = d . properties . map ( renderPropCard ) . join ( '' ) ;
} catch ( e ) {
list . innerHTML = ` <div class="empty-state"><div class="empty-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div><div class="empty-title">Fel</div><div class="empty-body"> ${ e . message } </div></div> ` ;
}
}
document . getElementById ( 'typeSegment' ) . addEventListener ( 'click' , e => {
const btn = e . target . closest ( '.seg-btn' ) ;
if ( ! btn ) return ;
document . querySelectorAll ( '#typeSegment .seg-btn' ) . forEach ( b => b . classList . remove ( 'active' ) ) ;
btn . classList . add ( 'active' ) ;
currentType = btn . dataset . type ;
loadProperties ( currentCity , currentType ) ;
} ) ;
let cityTimer ;
document . getElementById ( 'cityInput' ) . addEventListener ( 'input' , e => {
clearTimeout ( cityTimer ) ;
cityTimer = setTimeout ( ( ) => { currentCity = e . target . value . trim ( ) ; loadProperties ( currentCity , currentType ) ; } , 400 ) ;
} ) ;
document . getElementById ( 'marketCitySegment' ) . addEventListener ( 'click' , e => {
const btn = e . target . closest ( '.seg-btn' ) ;
if ( ! btn ) return ;
document . querySelectorAll ( '#marketCitySegment .seg-btn' ) . forEach ( b => b . classList . remove ( 'active' ) ) ;
btn . classList . add ( 'active' ) ;
marketCity = btn . dataset . city ;
loadMarket ( marketCity ) ;
} ) ;
async function loadMarket ( city ) {
const grid = document . getElementById ( 'statsGrid' ) ;
grid . innerHTML = '<div class="loading-center" style="grid-column:1/-1"><div class="spinner"></div></div>' ;
const q = city ? ` ?city= ${ encodeURIComponent ( city ) } ` : '' ;
try {
const r = await fetch ( ` ${ API } /analytics/market-overview ${ q } ` , { headers : AUTH } ) ;
const d = await r . json ( ) ;
const m = d . market _overview ;
grid . innerHTML = `
<div class="stat-card"><div class="stat-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div><div class="stat-value"> ${ m . total _properties } </div><div class="stat-label">Fastigheter</div></div>
<div class="stat-card"><div class="stat-icon"></div><div class="stat-value"> ${ fmtSEK ( m . total _assessed _value _sek ) } </div><div class="stat-label">Totalt taxvärde</div></div>
<div class="stat-card"><div class="stat-icon"></div><div class="stat-value"> ${ fmtSqm ( m . total _area _sqm ) } </div><div class="stat-label">Total yta</div></div>
<div class="stat-card"><div class="stat-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 12L6 8L9 11L14 5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 5H14V9" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div><div class="stat-value"> ${ m . avg _value _per _sqm ? . toLocaleString ( 'sv-SE' ) || '—' } </div><div class="stat-label">kr/kvm snitt</div></div> ` ;
const totalCnt = Object . values ( m . by _type ) . reduce ( ( s , v ) => s + v . count , 0 ) || 1 ;
document . getElementById ( 'typeDistribution' ) . innerHTML = Object . entries ( m . by _type ) . map ( ( [ t , v ] ) => {
const pct = Math . round ( v . count / totalCnt * 100 ) ;
const c = typeColor ( t ) ;
return ` <div style="padding:12px 16px;border-bottom:0.5px solid var(--sep)">
<div class="bar-row"><span class="bar-name"> ${ t } </span><span class="bar-count"> ${ v . count } · ${ fmtSEK ( v . total _value ) } </span></div>
<div class="bar-track"><div class="bar-fill" style="width: ${ pct } %;background: ${ c } "></div></div>
</div> ` ;
} ) . join ( '' ) || '<div style="padding:16px;color:var(--gray);text-align:center">Ingen data</div>' ;
const maxC = Math . max ( ... Object . values ( m . by _city ) . map ( v => v . count ) , 1 ) ;
document . getElementById ( 'cityBarChart' ) . innerHTML = Object . entries ( m . by _city ) . map ( ( [ c , v ] ) =>
` <div class="bar-item"><div class="bar-row"><span class="bar-name"> ${ c } </span><span class="bar-count"> ${ v . count } st</span></div><div class="bar-track"><div class="bar-fill" style="width: ${ Math . round ( v . count / maxC * 100 ) } %"></div></div></div> `
) . join ( '' ) ;
const tr = m . market _trends ;
document . getElementById ( 'trendQ1' ) . textContent = '+' + tr . q1 _2026 _price _growth _pct + '%' ;
document . getElementById ( 'trendAnnual' ) . textContent = '+' + tr . annual _appreciation _pct + '%' ;
document . getElementById ( 'trendTxn' ) . textContent = tr . transactions _ytd + ' st' ;
document . getElementById ( 'trendDays' ) . textContent = tr . avg _days _on _market + ' dagar' ;
const vEl = document . getElementById ( 'trendVacancy' ) ;
vEl . textContent = tr . vacancy _rate _commercial _pct + '%' ;
vEl . className = 'trend-val ' + ( tr . vacancy _rate _commercial _pct > 8 ? 'trend-neg' : 'trend-neutral' ) ;
document . getElementById ( 'trendCapRate' ) . textContent = tr . cap _rate _avg _pct + '%' ;
} catch ( e ) {
grid . innerHTML = ` <div class="loading-center" style="grid-column:1/-1"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> ${ e . message } </div> ` ;
}
}
async function registerProperty ( ) {
const address = document . getElementById ( 'regAddress' ) . value . trim ( ) ;
const type = document . getElementById ( 'regType' ) . value ;
const size = document . getElementById ( 'regSize' ) . value ;
const owner = document . getElementById ( 'regOwner' ) . value . trim ( ) ;
if ( ! address || ! type || ! size || ! owner ) { showToast ( 'Fyll i alla obligatoriska fält (*)' ) ; return ; }
const btn = document . getElementById ( 'registerBtn' ) ;
btn . disabled = true ; btn . textContent = 'Registrerar...' ;
try {
const body = {
address , type , size : parseInt ( size ) , owner ,
city : document . getElementById ( 'regCity' ) . value ,
} ;
const yr = document . getElementById ( 'regYear' ) . value ;
const fl = document . getElementById ( 'regFloors' ) . value ;
const val = document . getElementById ( 'regValue' ) . value ;
if ( yr ) body . built _year = parseInt ( yr ) ;
if ( fl ) body . floors = parseInt ( fl ) ;
if ( val ) body . assessed _value _sek = parseInt ( val ) ;
const r = await fetch ( ` ${ API } /property/register ` , { method : 'POST' , headers : AUTH , body : JSON . stringify ( body ) } ) ;
const d = await r . json ( ) ;
if ( ! r . ok ) throw new Error ( d . error || 'Registrering misslyckades' ) ;
showToast ( '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#00C853" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg> Registrerad: ' + d . property _id ) ;
clearForm ( ) ;
switchTab ( 'search' , document . querySelectorAll ( '.tab-item' ) [ 0 ] ) ;
currentCity = body . city ; document . getElementById ( 'cityInput' ) . value = body . city ;
loadProperties ( body . city , currentType ) ;
} catch ( e ) {
showToast ( '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FF3B30" stroke-width="3"><path d="M18 6L6 18M6 6l12 12"/></svg> ' + e . message ) ;
} finally {
btn . disabled = false ; btn . textContent = 'Registrera fastighet' ;
}
}
function clearForm ( ) {
[ 'regAddress' , 'regSize' , 'regOwner' , 'regYear' , 'regFloors' , 'regValue' ] . forEach ( id => document . getElementById ( id ) . value = '' ) ;
document . getElementById ( 'regType' ) . value = '' ;
document . getElementById ( 'regCity' ) . value = 'Stockholm' ;
}
loadProperties ( '' , '' ) ;
< / script >
< / body >
< / html >