elm-ssr 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +48 -342
  2. package/elm-src/ElmSsr/Island/Sse.elm +151 -0
  3. package/package.json +53 -24
  4. package/{packages/elm-ssr/src → src}/client-runtime/islands.ts +113 -15
  5. package/src/sse.ts +162 -0
  6. package/AGENTS.md +0 -289
  7. package/CHANGELOG.md +0 -87
  8. package/LICENSE +0 -21
  9. package/bun.lock +0 -259
  10. package/docker-compose.yml +0 -33
  11. package/docs/README.md +0 -51
  12. package/docs/backends.md +0 -146
  13. package/docs/cli.md +0 -117
  14. package/docs/effects.md +0 -91
  15. package/docs/getting-started.md +0 -94
  16. package/docs/islands.md +0 -197
  17. package/docs/loaders-and-actions.md +0 -241
  18. package/docs/middleware.md +0 -93
  19. package/docs/migrations.md +0 -143
  20. package/docs/routing.md +0 -108
  21. package/docs/sessions.md +0 -218
  22. package/docs/tasks.md +0 -149
  23. package/docs/testing.md +0 -84
  24. package/elm-ssr.config.json +0 -14
  25. package/examples/basic/elm.json +0 -27
  26. package/examples/basic/migrations/0001_guestbook.down.sql +0 -1
  27. package/examples/basic/migrations/0001_guestbook.sql +0 -10
  28. package/examples/basic/package.json +0 -10
  29. package/examples/basic/runtime.ts +0 -148
  30. package/examples/basic/src/Example/Basic/Islands/Counter.elm +0 -110
  31. package/examples/basic/src/Example/Basic/Islands/Observer.elm +0 -67
  32. package/examples/basic/src/Example/Basic/Islands/Tasks.elm +0 -151
  33. package/examples/basic/src/Example/Basic/Routes/Chart.elm +0 -87
  34. package/examples/basic/src/Example/Basic/Routes/Counter.elm +0 -42
  35. package/examples/basic/src/Example/Basic/Routes/Echo.elm +0 -76
  36. package/examples/basic/src/Example/Basic/Routes/Greet/Name_.elm +0 -37
  37. package/examples/basic/src/Example/Basic/Routes/Guestbook.elm +0 -86
  38. package/examples/basic/src/Example/Basic/Routes/Index.elm +0 -41
  39. package/examples/basic/src/Example/Basic/Routes/NotFound.elm +0 -37
  40. package/examples/basic/src/Example/Basic/Routes/Profile.elm +0 -112
  41. package/examples/basic/src/Example/Basic/Routes/Session.elm +0 -89
  42. package/examples/basic/src/Example/Basic/Routes/Status.elm +0 -90
  43. package/examples/basic/src/Example/Basic/View/Shared.elm +0 -60
  44. package/examples/basic/styles.ts +0 -204
  45. package/examples/basic/worker.ts +0 -3
  46. package/examples/crypto-dashboard/elm.json +0 -30
  47. package/examples/crypto-dashboard/package.json +0 -10
  48. package/examples/crypto-dashboard/runtime.ts +0 -97
  49. package/examples/crypto-dashboard/src/CryptoDashboard/Islands/MarketOverview.elm +0 -204
  50. package/examples/crypto-dashboard/src/CryptoDashboard/Islands/PriceChart.elm +0 -200
  51. package/examples/crypto-dashboard/src/CryptoDashboard/Routes/Index.elm +0 -67
  52. package/examples/crypto-dashboard/src/CryptoDashboard/Routes/NotFound.elm +0 -30
  53. package/examples/crypto-dashboard/src/CryptoDashboard/View/Shared.elm +0 -39
  54. package/examples/crypto-dashboard/styles.ts +0 -23
  55. package/examples/crypto-dashboard/worker.ts +0 -3
  56. package/llms.txt +0 -69
  57. package/packages/elm-ssr/README.md +0 -67
  58. package/packages/elm-ssr/package.json +0 -61
  59. package/scripts/benchmark.mjs +0 -60
  60. package/test/action.test.ts +0 -81
  61. package/test/adapters.test.ts +0 -173
  62. package/test/advanced-robustness.test.ts +0 -75
  63. package/test/app.test.ts +0 -209
  64. package/test/browser-island.test.ts +0 -184
  65. package/test/cli-migrate.test.ts +0 -97
  66. package/test/cli.test.ts +0 -94
  67. package/test/cookies.test.ts +0 -156
  68. package/test/crypto-dashboard.test.ts +0 -35
  69. package/test/effects.test.ts +0 -117
  70. package/test/http.test.ts +0 -50
  71. package/test/integration/redis-postgres.test.ts +0 -174
  72. package/test/island-runtime.test.ts +0 -214
  73. package/test/middleware.test.ts +0 -134
  74. package/test/migrations.test.ts +0 -244
  75. package/test/profile.test.ts +0 -159
  76. package/test/robustness.test.ts +0 -135
  77. package/test/serialize.test.ts +0 -92
  78. package/test/sessions.test.ts +0 -429
  79. package/test/svg.test.ts +0 -65
  80. package/tsconfig.json +0 -20
  81. package/wrangler.jsonc +0 -11
  82. /package/{packages/elm-ssr/bin → bin}/elm-ssr.mjs +0 -0
  83. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Action.elm +0 -0
  84. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document/Encode.elm +0 -0
  85. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document/Events.elm +0 -0
  86. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document.elm +0 -0
  87. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html/Attributes.elm +0 -0
  88. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html/Events.elm +0 -0
  89. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html.elm +0 -0
  90. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Island/Shared.elm +0 -0
  91. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Island.elm +0 -0
  92. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Loader.elm +0 -0
  93. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Page.elm +0 -0
  94. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Route.elm +0 -0
  95. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Runtime.elm +0 -0
  96. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Svg/Attributes.elm +0 -0
  97. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Svg.elm +0 -0
  98. /package/{packages/elm-ssr/lib → lib}/build.mjs +0 -0
  99. /package/{packages/elm-ssr/lib → lib}/migrate.mjs +0 -0
  100. /package/{packages/elm-ssr/lib → lib}/scaffold.mjs +0 -0
  101. /package/{packages/elm-ssr/lib → lib}/workspace.mjs +0 -0
  102. /package/{packages/elm-ssr/src → src}/app.ts +0 -0
  103. /package/{packages/elm-ssr/src → src}/backends.ts +0 -0
  104. /package/{packages/elm-ssr/src → src}/effects.ts +0 -0
  105. /package/{packages/elm-ssr/src → src}/http.ts +0 -0
  106. /package/{packages/elm-ssr/src → src}/middleware.ts +0 -0
  107. /package/{packages/elm-ssr/src → src}/migrations.ts +0 -0
  108. /package/{packages/elm-ssr/src → src}/protocol.ts +0 -0
  109. /package/{packages/elm-ssr/src → src}/render.ts +0 -0
  110. /package/{packages/elm-ssr/src → src}/request-handler.ts +0 -0
  111. /package/{packages/elm-ssr/src → src}/response-headers.ts +0 -0
  112. /package/{packages/elm-ssr/src → src}/serialize.ts +0 -0
  113. /package/{packages/elm-ssr/src → src}/sessions/crypto.ts +0 -0
  114. /package/{packages/elm-ssr/src → src}/sessions/effects.ts +0 -0
  115. /package/{packages/elm-ssr/src → src}/sessions/index.ts +0 -0
  116. /package/{packages/elm-ssr/src → src}/sessions/middleware.ts +0 -0
  117. /package/{packages/elm-ssr/src → src}/sessions/store.ts +0 -0
  118. /package/{packages/elm-ssr/src → src}/sessions/types.ts +0 -0
  119. /package/{packages/elm-ssr/src → src}/tasks.ts +0 -0
@@ -1,97 +0,0 @@
1
- import { createWorkerApp } from "elm-ssr";
2
- import { defaultEffectRunner, type EffectRunner } from "elm-ssr/effects";
3
- import { renderApp, type CompiledElmModule } from "elm-ssr/render";
4
- import type { RouteCatalog } from "elm-ssr/http";
5
- import { islands, bundleSource } from "../../generated/examples/crypto-dashboard/islands-manifest";
6
- import { stylesheet } from "./styles";
7
- // @ts-expect-error Generated at build time.
8
- import ElmRuntime from "../../generated/examples/crypto-dashboard/app.mjs";
9
-
10
- const elmModule = ElmRuntime as CompiledElmModule;
11
-
12
- export const routes: RouteCatalog = {
13
- pages: [
14
- {
15
- path: "/",
16
- methods: ["GET", "HEAD"],
17
- description: "Crypto Dashboard landing page."
18
- }
19
- ],
20
- assets: [
21
- {
22
- path: "/styles.css",
23
- methods: ["GET", "HEAD"],
24
- description: "Tailwind base styles."
25
- },
26
- {
27
- path: "/__elm-ssr/islands.js",
28
- methods: ["GET", "HEAD"],
29
- description: "Island loader runtime."
30
- },
31
- {
32
- path: "/__elm-ssr/islands-bundle.js",
33
- methods: ["GET", "HEAD"],
34
- description: "Shared island bundle (Charts, etc)."
35
- }
36
- ],
37
- utility: [
38
- {
39
- path: "/health",
40
- methods: ["GET", "HEAD"],
41
- description: "Liveness endpoint."
42
- }
43
- ],
44
- api: [
45
- {
46
- path: "/api/health",
47
- methods: ["GET", "HEAD"],
48
- description: "JSON health payload."
49
- },
50
- {
51
- path: "/api/render",
52
- methods: ["GET", "HEAD"],
53
- description: "SSR preview endpoint."
54
- }
55
- ]
56
- };
57
-
58
- export const createFlags = ({ request, path, formData }: { request?: Request; url?: URL; path: string; formData?: Record<string, string> }) => {
59
- const [pathname, search = ""] = path.split("?");
60
-
61
- return {
62
- method: request?.method ?? "GET",
63
- path: pathname,
64
- query: Object.fromEntries(new URLSearchParams(search)),
65
- formData: formData ?? {}
66
- };
67
- };
68
-
69
- // Mock CoinGecko API for stable testing and fast builds
70
- export const cryptoEffects: EffectRunner = async (effect, context) => {
71
- if (effect.kind === "fetchJson" && typeof effect.payload.url === "string" && effect.payload.url.includes("api.coingecko.com")) {
72
- return {
73
- ok: true,
74
- value: [
75
- { id: "bitcoin", symbol: "btc", name: "Bitcoin", current_price: 65000.0, price_change_percentage_24h: 2.5 },
76
- { id: "ethereum", symbol: "eth", name: "Ethereum", current_price: 3500.0, price_change_percentage_24h: -1.2 },
77
- { id: "cardano", symbol: "ada", name: "Cardano", current_price: 0.45, price_change_percentage_24h: 0.5 },
78
- { id: "solana", symbol: "sol", name: "Solana", current_price: 145.0, price_change_percentage_24h: 5.8 }
79
- ]
80
- };
81
- }
82
-
83
- return defaultEffectRunner(effect, context);
84
- };
85
-
86
- export const renderPath = async (path: string) =>
87
- renderApp(elmModule, createFlags({ path }), { effects: cryptoEffects });
88
-
89
- export const worker = createWorkerApp({
90
- elmModule,
91
- islands,
92
- islandsBundle: bundleSource,
93
- stylesheet,
94
- routes,
95
- createFlags,
96
- effects: cryptoEffects
97
- });
@@ -1,204 +0,0 @@
1
- module CryptoDashboard.Islands.MarketOverview exposing
2
- ( embed
3
- , Flags, Model, Msg
4
- , encodeFlags
5
- , init, main, subscriptions, update, view
6
- )
7
-
8
- import Browser
9
- import ElmSsr.Html as SsrHtml exposing (Node)
10
- import ElmSsr.Html.Attributes as SsrAttributes
11
- import ElmSsr.Island as Island
12
- import ElmSsr.Island.Shared as SharedBus
13
- import Html exposing (Html, div, h3, p, span, text)
14
- import Html.Attributes as HtmlAttr exposing (class)
15
- import Html.Events exposing (onClick)
16
- import Http
17
- import Json.Decode as Decode
18
- import Json.Encode as Encode
19
- import Time
20
-
21
-
22
- type alias Coin =
23
- { id : String
24
- , symbol : String
25
- , name : String
26
- , price : Float
27
- , change24h : Float
28
- }
29
-
30
-
31
- type alias Flags =
32
- { coins : List Coin }
33
-
34
-
35
- type alias Model =
36
- { coins : List Coin
37
- , selectedId : String
38
- }
39
-
40
-
41
- type Msg
42
- = SelectCoin String
43
- | Refresh
44
- | GotCoins (Result Http.Error (List Coin))
45
-
46
-
47
- embed : Flags -> SsrHtml.Node msg
48
- embed =
49
- Island.embed "MarketOverview"
50
- { encodeFlags = encodeFlags
51
- , fallback = fallback
52
- , id = Just "global-market-overview"
53
- }
54
-
55
-
56
- fallback : Flags -> List (SsrHtml.Node msg)
57
- fallback flags =
58
- [ SsrHtml.div [ SsrAttributes.class "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" ]
59
- (List.map
60
- (\coin ->
61
- SsrHtml.div [ SsrAttributes.class "bg-slate-900 rounded-xl border border-slate-800 p-5 shadow-sm opacity-50" ]
62
- [ SsrHtml.h3 [ SsrAttributes.class "font-bold text-slate-400 text-xs uppercase tracking-wider" ] [ SsrHtml.text coin.name ] ]
63
- )
64
- flags.coins
65
- )
66
- ]
67
-
68
-
69
- encodeFlags : Flags -> Encode.Value
70
- encodeFlags flags =
71
- Encode.object
72
- [ ( "coins"
73
- , Encode.list
74
- (\c ->
75
- Encode.object
76
- [ ( "id", Encode.string c.id )
77
- , ( "symbol", Encode.string c.symbol )
78
- , ( "name", Encode.string c.name )
79
- , ( "price", Encode.float c.price )
80
- , ( "change24h", Encode.float c.change24h )
81
- ]
82
- )
83
- flags.coins
84
- )
85
- ]
86
-
87
-
88
- main : Program Flags Model Msg
89
- main =
90
- Browser.element
91
- { init = init
92
- , update = update
93
- , view = view
94
- , subscriptions = subscriptions
95
- }
96
-
97
-
98
- init : Flags -> ( Model, Cmd Msg )
99
- init flags =
100
- -- Flags carry the server-rendered snapshot; refresh live from the client.
101
- ( { coins = flags.coins, selectedId = "bitcoin" }, fetchMarkets )
102
-
103
-
104
- update : Msg -> Model -> ( Model, Cmd Msg )
105
- update msg model =
106
- case msg of
107
- SelectCoin id ->
108
- ( { model | selectedId = id }
109
- , SharedBus.broadcast "coin-selected" (Encode.string id)
110
- )
111
-
112
- Refresh ->
113
- ( model, fetchMarkets )
114
-
115
- GotCoins (Ok coins) ->
116
- ( { model | coins = coins }, Cmd.none )
117
-
118
- GotCoins (Err _) ->
119
- ( model, Cmd.none )
120
-
121
-
122
- subscriptions : Model -> Sub Msg
123
- subscriptions _ =
124
- Time.every 15000 (\_ -> Refresh)
125
-
126
-
127
- fetchMarkets : Cmd Msg
128
- fetchMarkets =
129
- Http.get
130
- { url = marketsUrl
131
- , expect = Http.expectJson GotCoins marketDecoder
132
- }
133
-
134
-
135
- marketsUrl : String
136
- marketsUrl =
137
- "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin,ethereum,cardano,solana&order=market_cap_desc"
138
-
139
-
140
- marketDecoder : Decode.Decoder (List Coin)
141
- marketDecoder =
142
- Decode.list
143
- (Decode.map5 Coin
144
- (Decode.field "id" Decode.string)
145
- (Decode.field "symbol" Decode.string)
146
- (Decode.field "name" Decode.string)
147
- (Decode.field "current_price" Decode.float)
148
- (Decode.field "price_change_percentage_24h" Decode.float)
149
- )
150
-
151
-
152
- view : Model -> Html Msg
153
- view model =
154
- div [ class "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" ]
155
- (List.map (coinCard model.selectedId) model.coins)
156
-
157
-
158
- coinCard : String -> Coin -> Html Msg
159
- coinCard selectedId coin =
160
- let
161
- isSelected =
162
- selectedId == coin.id
163
- in
164
- div
165
- [ class
166
- ("bg-slate-900 rounded-xl border p-5 shadow-sm transition-all group cursor-pointer "
167
- ++ (if isSelected then
168
- "border-indigo-500 ring-1 ring-indigo-500/50 shadow-indigo-500/20 shadow-lg"
169
-
170
- else
171
- "border-slate-800 hover:border-slate-700"
172
- )
173
- )
174
- , onClick (SelectCoin coin.id)
175
- ]
176
- [ div [ class "flex justify-between items-start mb-4" ]
177
- [ div []
178
- [ h3 [ class "font-bold text-slate-400 text-xs uppercase tracking-wider" ] [ text coin.name ]
179
- , p [ class ("text-2xl font-bold transition-colors " ++ (if isSelected then "text-indigo-400" else "text-white group-hover:text-indigo-400")) ] [ text ("$" ++ String.fromFloat coin.price) ]
180
- ]
181
- , span
182
- [ class
183
- (if coin.change24h >= 0 then
184
- "text-emerald-400 bg-emerald-400/10 px-2 py-1 rounded text-xs font-bold"
185
-
186
- else
187
- "text-rose-400 bg-rose-400/10 px-2 py-1 rounded text-xs font-bold"
188
- )
189
- ]
190
- [ text (String.fromFloat (roundTo 2 coin.change24h) ++ "%") ]
191
- ]
192
- , div [ class "h-1 w-full bg-slate-800 rounded-full overflow-hidden" ]
193
- [ div [ class "h-full bg-indigo-500 rounded-full", HtmlAttr.style "width" (String.fromFloat (clamp 0 100 (50 + coin.change24h)) ++ "%") ] []
194
- ]
195
- ]
196
-
197
-
198
- roundTo : Int -> Float -> Float
199
- roundTo decimalPlaces num =
200
- let
201
- factor =
202
- 10 ^ decimalPlaces |> toFloat
203
- in
204
- (num * factor |> round |> toFloat) / factor
@@ -1,200 +0,0 @@
1
- module CryptoDashboard.Islands.PriceChart exposing
2
- ( embed
3
- , Flags, Model, Msg
4
- , encodeFlags
5
- , init, main, subscriptions, update, view
6
- )
7
-
8
- import Browser
9
- import ElmSsr.Html as SsrHtml exposing (Node)
10
- import ElmSsr.Html.Attributes as SsrAttributes
11
- import ElmSsr.Island as Island
12
- import ElmSsr.Island.Shared as SharedBus
13
- import Html exposing (Html, div, text)
14
- import Html.Attributes as HtmlAttr
15
- import Http
16
- import Json.Decode as Decode
17
- import Json.Encode as Encode
18
- import Svg exposing (Svg, svg)
19
- import Svg.Attributes as SvgAttr
20
- import Time
21
-
22
-
23
- embed : Flags -> SsrHtml.Node msg
24
- embed =
25
- Island.embed "PriceChart"
26
- { encodeFlags = encodeFlags
27
- , fallback = fallback
28
- , id = Just "btc-price-chart"
29
- }
30
-
31
-
32
- type alias Flags =
33
- { coinId : String }
34
-
35
-
36
- type alias DataPoint =
37
- { time : Float
38
- , price : Float
39
- }
40
-
41
-
42
- type Model
43
- = Loading String
44
- | Loaded String (List DataPoint)
45
- | Error String String
46
-
47
-
48
- type Msg
49
- = GotData String (Result Http.Error (List DataPoint))
50
- | OnGlobalEvent SharedBus.GlobalEvent
51
- | Refresh
52
-
53
-
54
- encodeFlags : Flags -> Encode.Value
55
- encodeFlags flags =
56
- Encode.object [ ( "coinId", Encode.string flags.coinId ) ]
57
-
58
-
59
- fallback : Flags -> List (SsrHtml.Node msg)
60
- fallback _ =
61
- [ SsrHtml.div [ SsrAttributes.class "h-64 flex items-center justify-center bg-slate-900/50 rounded-lg animate-pulse" ]
62
- [ SsrHtml.text "Loading market data..." ]
63
- ]
64
-
65
-
66
- main : Program Flags Model Msg
67
- main =
68
- Browser.element
69
- { init = init
70
- , update = update
71
- , view = view
72
- , subscriptions = subscriptions
73
- }
74
-
75
-
76
- init : Flags -> ( Model, Cmd Msg )
77
- init flags =
78
- ( Loading flags.coinId, fetchData flags.coinId )
79
-
80
-
81
- fetchData : String -> Cmd Msg
82
- fetchData coinId =
83
- Http.get
84
- { url = "https://api.coingecko.com/api/v3/coins/" ++ coinId ++ "/market_chart?vs_currency=usd&days=7"
85
- , expect = Http.expectJson (GotData coinId) chartDecoder
86
- }
87
-
88
-
89
- chartDecoder : Decode.Decoder (List DataPoint)
90
- chartDecoder =
91
- Decode.field "prices"
92
- (Decode.list
93
- (Decode.map2 DataPoint
94
- (Decode.index 0 Decode.float)
95
- (Decode.index 1 Decode.float)
96
- )
97
- )
98
-
99
-
100
- update : Msg -> Model -> ( Model, Cmd Msg )
101
- update msg model =
102
- case msg of
103
- GotData coinId (Ok data) ->
104
- ( Loaded coinId data, Cmd.none )
105
-
106
- GotData coinId (Err _) ->
107
- ( Error coinId "Failed to load chart", Cmd.none )
108
-
109
- OnGlobalEvent event ->
110
- if event.tag == "coin-selected" then
111
- case Decode.decodeValue Decode.string event.payload of
112
- Ok newCoinId ->
113
- ( Loading newCoinId, fetchData newCoinId )
114
-
115
- Err _ ->
116
- ( model, Cmd.none )
117
-
118
- else
119
- ( model, Cmd.none )
120
-
121
- Refresh ->
122
- -- Silently refetch the current coin (keep showing the last chart).
123
- ( model, fetchData (currentCoin model) )
124
-
125
-
126
- currentCoin : Model -> String
127
- currentCoin model =
128
- case model of
129
- Loading coinId ->
130
- coinId
131
-
132
- Loaded coinId _ ->
133
- coinId
134
-
135
- Error coinId _ ->
136
- coinId
137
-
138
-
139
- subscriptions : Model -> Sub Msg
140
- subscriptions _ =
141
- Sub.batch
142
- [ SharedBus.listen OnGlobalEvent
143
- , Time.every 15000 (\_ -> Refresh)
144
- ]
145
-
146
-
147
- view : Model -> Html Msg
148
- view model =
149
- div [ HtmlAttr.class "w-full" ]
150
- [ case model of
151
- Loading id ->
152
- div [ HtmlAttr.class "h-64 flex items-center justify-center text-slate-500 italic" ] [ text ("Fetching " ++ id ++ " history...") ]
153
-
154
- Loaded _ data ->
155
- renderChart data
156
-
157
- Error _ msg ->
158
- div [ HtmlAttr.class "h-64 flex items-center justify-center text-rose-500" ] [ text msg ]
159
- ]
160
-
161
-
162
- renderChart : List DataPoint -> Html Msg
163
- renderChart data =
164
- let
165
- w = 800
166
- h = 250
167
- padding = 30
168
-
169
- prices = List.map .price data
170
- minPrice = List.minimum prices |> Maybe.withDefault 0
171
- maxPrice = List.maximum prices |> Maybe.withDefault 1
172
- priceDiff = maxPrice - minPrice
173
-
174
- times = List.map .time data
175
- minTime = List.minimum times |> Maybe.withDefault 0
176
- maxTime = List.maximum times |> Maybe.withDefault 1
177
- timeDiff = maxTime - minTime
178
-
179
- toX t = padding + ((t - minTime) / (if timeDiff == 0 then 1 else timeDiff) * (w - 2 * padding))
180
- toY p = (h - padding) - ((p - minPrice) / (if priceDiff == 0 then 1 else priceDiff) * (h - 2 * padding))
181
-
182
- points =
183
- data
184
- |> List.map (\d -> String.fromFloat (toX d.time) ++ "," ++ String.fromFloat (toY d.price))
185
- |> String.join " "
186
- in
187
- svg
188
- [ SvgAttr.viewBox ("0 0 " ++ String.fromFloat w ++ " " ++ String.fromFloat h)
189
- , SvgAttr.class "w-full overflow-visible"
190
- ]
191
- [ Svg.polyline
192
- [ SvgAttr.points points
193
- , SvgAttr.fill "none"
194
- , SvgAttr.stroke "#6366f1"
195
- , SvgAttr.strokeWidth "3"
196
- , SvgAttr.strokeLinecap "round"
197
- , SvgAttr.strokeLinejoin "round"
198
- ]
199
- []
200
- ]
@@ -1,67 +0,0 @@
1
- module CryptoDashboard.Routes.Index exposing (page, action)
2
-
3
- import CryptoDashboard.Islands.MarketOverview as MarketOverview
4
- import CryptoDashboard.Islands.PriceChart as PriceChart
5
- import CryptoDashboard.View.Shared as Shared
6
- import ElmSsr.Action as Action exposing (Action)
7
- import ElmSsr.Document exposing (Document)
8
- import ElmSsr.Html exposing (Node, div, h2, section, text)
9
- import ElmSsr.Html.Attributes exposing (class)
10
- import ElmSsr.Loader as Loader exposing (Loader)
11
- import ElmSsr.Page as Page
12
- import ElmSsr.Route exposing (Request)
13
- import Json.Decode as Decode
14
-
15
-
16
- type alias Coin =
17
- { id : String
18
- , symbol : String
19
- , name : String
20
- , price : Float
21
- , change24h : Float
22
- }
23
-
24
-
25
- type alias MarketData =
26
- { coins : List Coin
27
- }
28
-
29
-
30
- page : Request -> Loader (Document Never)
31
- page _ =
32
- Loader.fetchJson { url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin,ethereum,cardano,solana&order=market_cap_desc", decoder = coinsDecoder }
33
- |> Loader.map (\coins -> view { coins = coins })
34
-
35
-
36
- action : Request -> Action (Document Never)
37
- action _ =
38
- Action.fail 405 "Method not allowed"
39
-
40
-
41
- coinsDecoder : Decode.Decoder (List Coin)
42
- coinsDecoder =
43
- Decode.list
44
- (Decode.map5 Coin
45
- (Decode.field "id" Decode.string)
46
- (Decode.field "symbol" Decode.string)
47
- (Decode.field "name" Decode.string)
48
- (Decode.field "current_price" Decode.float)
49
- (Decode.field "price_change_percentage_24h" Decode.float)
50
- )
51
-
52
-
53
- view : MarketData -> Document Never
54
- view data =
55
- Page.page
56
- { title = "Crypto Dashboard | CryptoPulse"
57
- , head = Shared.head
58
- , body =
59
- [ Shared.shell "Market Overview"
60
- [ MarketOverview.embed { coins = data.coins }
61
- , section [ class "mt-12 bg-slate-900 rounded-2xl border border-slate-800 p-6 shadow-xl" ]
62
- [ h2 [ class "text-xl font-bold text-white mb-6" ] [ text "Real-time Price Trend" ]
63
- , PriceChart.embed { coinId = "bitcoin" }
64
- ]
65
- ]
66
- ]
67
- }
@@ -1,30 +0,0 @@
1
- module CryptoDashboard.Routes.NotFound exposing (page, action)
2
-
3
- -- File-based routing: NotFound is the fallback when no other route matches.
4
-
5
- import ElmSsr.Action as Action exposing (Action)
6
- import ElmSsr.Document exposing (Document)
7
- import ElmSsr.Html exposing (p, text)
8
- import ElmSsr.Loader as Loader exposing (Loader)
9
- import ElmSsr.Page as Page
10
- import ElmSsr.Route exposing (Request)
11
- import CryptoDashboard.View.Shared as Shared
12
-
13
-
14
- page : Request -> Loader (Document Never)
15
- page _ =
16
- Loader.succeed view
17
-
18
-
19
- action : Request -> Action (Document Never)
20
- action _ =
21
- Action.fail 405 "Method not allowed"
22
-
23
-
24
- view : Document Never
25
- view =
26
- Page.notFound
27
- { title = "Not Found | elm-ssr"
28
- , head = Shared.head
29
- , body = [ Shared.shell "404" [ p [] [ text "This route does not exist." ] ] ]
30
- }
@@ -1,39 +0,0 @@
1
- module CryptoDashboard.View.Shared exposing (head, shell)
2
-
3
- import ElmSsr.Html as Html exposing (Node, a, div, h1, header, main_, nav, p, section, span, text)
4
- import ElmSsr.Html.Attributes as SsrAttributes exposing (class, href)
5
- import ElmSsr.Page as Page
6
-
7
-
8
- head : List (Node msg)
9
- head =
10
- [ Page.metaCharset "utf-8"
11
- , Page.metaViewport "width=device-width, initial-scale=1"
12
- , Html.script [ class "tailwind-cdn", SsrAttributes.src "https://cdn.tailwindcss.com" ] []
13
- , Page.stylesheet "/styles.css"
14
- ]
15
-
16
-
17
- shell : String -> List (Node msg) -> Node msg
18
- shell heading body =
19
- div [ class "min-h-screen bg-slate-950 text-slate-100 font-sans" ]
20
- [ nav [ class "border-b border-slate-800 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50" ]
21
- [ div [ class "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between" ]
22
- [ div [ class "flex items-center gap-2" ]
23
- [ div [ class "w-8 h-8 bg-indigo-500 rounded-lg flex items-center justify-center font-bold text-white shadow-lg shadow-indigo-500/20" ] [ text "Σ" ]
24
- , span [ class "font-bold text-xl tracking-tight" ] [ text "CryptoPulse" ]
25
- ]
26
- , div [ class "flex gap-4 text-sm font-medium text-slate-400" ]
27
- [ a [ class "hover:text-white transition-colors", href "/" ] [ text "Dashboard" ]
28
- , a [ class "hover:text-white transition-colors", href "/market" ] [ text "Markets" ]
29
- ]
30
- ]
31
- ]
32
- , main_ [ class "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" ]
33
- (header [ class "mb-8" ]
34
- [ h1 [ class "text-3xl font-bold text-white tracking-tight" ] [ text heading ]
35
- , p [ class "text-slate-400 mt-1" ] [ text "Real-time market insights powered by Elm SSR" ]
36
- ]
37
- :: body
38
- )
39
- ]
@@ -1,23 +0,0 @@
1
- export const stylesheet = `
2
- /* Tailwind is loaded via CDN in the head, so we only need base resets or custom utilities here. */
3
- @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;700&display=swap');
4
-
5
- :root {
6
- color-scheme: dark;
7
- }
8
-
9
- body {
10
- margin: 0;
11
- background-color: #020617; /* bg-slate-950 */
12
- }
13
-
14
- /* Custom chart animations */
15
- .animate-pulse {
16
- animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
17
- }
18
-
19
- @keyframes pulse {
20
- 0%, 100% { opacity: 1; }
21
- 50% { opacity: .5; }
22
- }
23
- `;
@@ -1,3 +0,0 @@
1
- import { worker } from "./runtime";
2
-
3
- export default worker;