feat(01): Simple initial landing
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
BIN
summercms/resources/public/assets/images/logo.png
Normal file
BIN
summercms/resources/public/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
216
summercms/resources/public/landing.html
Normal file
216
summercms/resources/public/landing.html
Normal 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>☀</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>
|
||||||
@@ -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
|
||||||
|
|||||||
62
summercms/src/api/LandingRoutes.scala
Normal file
62
summercms/src/api/LandingRoutes.scala
Normal 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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user