feat(01): Simple initial landing

This commit is contained in:
Jakub Zych
2026-02-05 13:01:34 +01:00
parent d502bb19a5
commit 7b5fe94e53
8 changed files with 295 additions and 11 deletions

View File

@@ -27,7 +27,10 @@ object summercms extends ScalaModule {
// Flyway for migrations // Flyway for migrations
mvn"org.flywaydb:flyway-core:10.21.0", mvn"org.flywaydb:flyway-core:10.21.0",
mvn"org.flywaydb:flyway-database-postgresql:10.21.0" mvn"org.flywaydb:flyway-database-postgresql:10.21.0",
// Logging
mvn"org.slf4j:slf4j-simple:2.0.9"
) )
def scalacOptions = Seq( def scalacOptions = Seq(

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -0,0 +1,216 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SummerCMS</title>
<meta name="description" content="SummerCMS - A new dawn in content management.">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://summercms.io/">
<meta property="og:title" content="SummerCMS - Coming Soon">
<meta property="og:description" content="A new dawn in content management.">
<meta property="og:image" content="https://summercms.io/logo.png">
<meta property="og:site_name" content="SummerCMS">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="SummerCMS - Coming Soon">
<meta name="twitter:description" content="A new dawn in content management.">
<meta name="twitter:image" content="https://summercms.io/logo.png">
<link rel="icon" type="image/png" href="/assets/images/logo.png">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #1e2e47;
color: white;
}
.container { text-align: center; padding: 2rem; }
.logo {
width: 120px;
height: 120px;
margin-bottom: 2rem;
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3));
}
.sun {
font-size: 80px;
margin-bottom: 1rem;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
h1 {
font-size: 3rem;
font-weight: 300;
letter-spacing: 0.5rem;
margin-bottom: 0.5rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.tagline {
font-size: 1.1rem;
opacity: 0.9;
margin-bottom: 2rem;
}
.status {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
padding: 0.5rem 1.5rem;
border-radius: 2rem;
font-size: 0.9rem;
backdrop-filter: blur(10px);
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
background: #4ade80;
border-radius: 50%;
margin-right: 0.5rem;
animation: blink 1.5s ease-in-out infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.links {
margin-top: 2rem;
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.links a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 0.5rem;
transition: all 0.2s;
}
.links a:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
.version {
position: fixed;
bottom: 1rem;
opacity: 0.6;
font-size: 0.8rem;
}
/* Animated background rays */
.rays {
position: absolute;
top: 50%;
left: 50%;
width: 200%;
height: 200%;
transform: translate(-50%, -50%);
background: conic-gradient(
from 0deg,
transparent 0deg,
rgba(255, 200, 87, 0.03) 10deg,
transparent 20deg,
transparent 30deg,
rgba(255, 200, 87, 0.03) 40deg,
transparent 50deg,
transparent 60deg,
rgba(255, 200, 87, 0.03) 70deg,
transparent 80deg,
transparent 90deg,
rgba(255, 200, 87, 0.03) 100deg,
transparent 110deg,
transparent 120deg,
rgba(255, 200, 87, 0.03) 130deg,
transparent 140deg,
transparent 150deg,
rgba(255, 200, 87, 0.03) 160deg,
transparent 170deg,
transparent 180deg,
rgba(255, 200, 87, 0.03) 190deg,
transparent 200deg,
transparent 210deg,
rgba(255, 200, 87, 0.03) 220deg,
transparent 230deg,
transparent 240deg,
rgba(255, 200, 87, 0.03) 250deg,
transparent 260deg,
transparent 270deg,
rgba(255, 200, 87, 0.03) 280deg,
transparent 290deg,
transparent 300deg,
rgba(255, 200, 87, 0.03) 310deg,
transparent 320deg,
transparent 330deg,
rgba(255, 200, 87, 0.03) 340deg,
transparent 350deg,
transparent 360deg
);
animation: rotate 60s linear infinite;
pointer-events: none;
}
@keyframes rotate {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.container {
text-align: center;
z-index: 1;
padding: 2rem;
}
.logo-wrapper {
position: relative;
display: inline-block;
margin-bottom: 2rem;
}
.logo {
width: 140px;
height: 140px;
object-fit: contain;
filter: drop-shadow(0 0 30px rgba(255, 200, 87, 0.4));
animation: pulse 3s ease-in-out infinite, float 4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { filter: drop-shadow(0 0 30px rgba(255, 200, 87, 0.4)); }
50% { filter: drop-shadow(0 0 50px rgba(255, 200, 87, 0.6)); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
</style>
</head>
<body>
<div class="container">
<img src="/assets/images/logo.png" alt="SummerCMS" class="logo" onerror="this.outerHTML='<div class=sun>&#9728;</div>'">
<h1>SUMMERCMS</h1>
<p class="tagline">A New Dawn in Content Management</p>
<div class="status">
<span class="status-dot"></span>
Server Running
</div>
<div class="links">
<a href="/health">Health Check</a>
<a href="/ready">Readiness Check</a>
</div>
</div>
<div class="version">SummerCMS v0.1.0 | Scala + ZIO</div>
</body>
</html>

View File

@@ -10,13 +10,16 @@ object Main extends ZIOAppDefault {
private val banner: String = private val banner: String =
""" """
| . |
| \ | / | | .
| '-.ooooo.-' | `. * | .'
| --- ooooo --- | `. ._|_* .' .
| .-'ooooo'-. | . * .' `. *
| / | \ | -------| |-------
| ' | . *`.___.' * .
| .' |* `. *
| .' * | . `.
| . |
| |
| S U M M E R C M S | S U M M E R C M S
|""".stripMargin |""".stripMargin

View File

@@ -0,0 +1,62 @@
package api
import zio.*
import zio.http.*
import scala.io.Source
/** Landing page and static asset routes
*/
object LandingRoutes {
private def loadResource(path: String): Option[String] =
Option(getClass.getClassLoader.getResourceAsStream(path)).map { stream =>
val content = Source.fromInputStream(stream, "UTF-8").mkString
stream.close()
content
}
private def serveLanding: ZIO[Any, Nothing, Response] =
ZIO.succeed {
loadResource("public/landing.html") match {
case Some(html) =>
Response(
status = Status.Ok,
headers = Headers(Header.ContentType(MediaType.text.html)),
body = Body.fromString(html)
)
case None =>
Response.text("Landing page not found").status(Status.InternalServerError)
}
}
private def serveImage(filename: String): ZIO[Any, Nothing, Response] =
ZIO.attemptBlocking {
val path = s"public/assets/images/$filename"
val stream = getClass.getClassLoader.getResourceAsStream(path)
if (stream == null) {
Response.notFound
} else {
val bytes = stream.readAllBytes()
stream.close()
val mediaType = if (filename.endsWith(".png")) MediaType.image.png
else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) MediaType.image.jpeg
else if (filename.endsWith(".svg")) MediaType.image.`svg+xml`
else if (filename.endsWith(".gif")) MediaType.image.gif
else MediaType.application.`octet-stream`
Response(
status = Status.Ok,
headers = Headers(Header.ContentType(mediaType)),
body = Body.fromChunk(Chunk.fromArray(bytes))
)
}
}.catchAll(_ => ZIO.succeed(Response.notFound))
val routes: Routes[Any, Response] =
Routes(
Method.GET / Root -> handler(serveLanding),
Method.GET / "assets" / "images" / string("filename") -> handler { (filename: String, _: Request) =>
serveImage(filename)
}
)
}

View File

@@ -10,6 +10,6 @@ import javax.sql.DataSource
object Routes { object Routes {
val routes: Routes[DataSource, Response] = val routes: Routes[DataSource, Response] =
HealthRoutes.routes LandingRoutes.routes ++ HealthRoutes.routes
} }