Compare commits

...

36 commits
v0.1.0 ... main

Author SHA1 Message Date
08c8c0f0f7
chore: bump to 0.2.2
All checks were successful
Checks / check (push) Successful in -6m20s
Build and Push Docker Image / Build and Push Image (push) Successful in -5m27s
2024-12-23 19:46:03 +02:00
1ccab387a9
fix: wrong day of the week + calendar not updating
Some checks failed
Build and Push Docker Image / Build and Push Image (push) Waiting to run
Checks / check (push) Has been cancelled
2024-12-23 19:45:10 +02:00
92715e9a97
chore: bump to 0.2.1
All checks were successful
Checks / check (push) Successful in -6m19s
Build and Push Docker Image / Build and Push Image (push) Successful in -5m23s
2024-12-23 19:10:23 +02:00
3deeafd084
style(calendar): make the size persistent
All checks were successful
Checks / check (push) Successful in -6m13s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m9s
2024-12-23 19:02:18 +02:00
9a41ca7e98
style: mix format
All checks were successful
Checks / check (push) Successful in -6m22s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m16s
2024-12-23 00:13:58 +02:00
4ef23a5ec9
fix: dark mode for calendar
Some checks failed
Build and Push Docker Image / Build and Push Image (push) Waiting to run
Checks / check (push) Has been cancelled
2024-12-23 00:13:29 +02:00
a724826a59
style: mix format 2024-12-23 00:10:10 +02:00
c60a5e0f12
fix: wrong day of the week
Some checks failed
Checks / check (push) Failing after -6m21s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m4s
2024-12-22 22:53:56 +02:00
223939da6f
feat: calendar view
Some checks failed
Checks / check (push) Failing after -6m20s
Build and Push Docker Image / Build and Push Image (push) Has been cancelled
2024-12-22 22:52:30 +02:00
d5f7ddf250
chore: update README
All checks were successful
Checks / check (push) Successful in -5m35s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m50s
2024-12-22 22:18:23 +02:00
f18fc0ab63
chore: bump to 0.2.0
All checks were successful
Checks / check (push) Successful in -4m26s
Build and Push Docker Image / Build and Push Image (push) Successful in -3m52s
i finally remembered that with each feat commit you're supposed to bump
the minor, not the patch. so i bumped to 0.2.0
2024-12-14 00:35:26 +02:00
41c67fe35e
style: remove "Phoenix Framework" from tab title 2024-12-14 00:34:04 +02:00
ff9a3a8e92
trans: Translate Register and Log in
kinda forgot about those
2024-12-14 00:33:01 +02:00
053bfed5e6
chore: bump to 0.1.4
All checks were successful
Checks / check (push) Successful in -6m23s
Build and Push Docker Image / Build and Push Image (push) Successful in -5m25s
2024-12-12 18:59:36 +02:00
b5d52426b6 Merge pull request 'feat: add i18n' (#3) from i18n into main
All checks were successful
Checks / check (push) Successful in -6m24s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m1s
Reviewed-on: #3
2024-12-12 18:56:58 +02:00
65308f8bda
style: mix format
All checks were successful
Checks / check (pull_request) Successful in -6m26s
2024-12-12 18:52:36 +02:00
f7f0ebbb25
trans: errors.po 2024-12-12 18:52:07 +02:00
7380d3da3c
trans: switch to ngettext for "n days left"
Some checks failed
Checks / check (pull_request) Failing after -6m14s
2024-12-12 18:19:53 +02:00
a5df93e4b7 Merge branch 'main' into i18n
Some checks failed
Checks / check (pull_request) Failing after -6m27s
2024-12-11 22:38:13 +02:00
bb455f793a
chore: update phoenix_live_view to 1.0.0
Some checks failed
Checks / check (push) Failing after -6m27s
Build and Push Docker Image / Build and Push Image (push) Successful in -5m27s
2024-12-11 22:36:47 +02:00
094b0f0324
style: mix format
All checks were successful
Checks / check (pull_request) Successful in -6m20s
2024-12-11 22:22:40 +02:00
9e9d8ffdaf
trans: settings and new exam modal
Some checks failed
Checks / check (pull_request) Failing after -6m8s
translated those to russian
2024-12-11 22:18:34 +02:00
cf28aa5295
feat: add i18n
Some checks failed
Checks / check (pull_request) Failing after -6m26s
right not, the most of the app is translated, but there are some parts that are not translated yet, like the settings page.
2024-12-10 00:39:52 +02:00
c254254f57
style: fix formatting
All checks were successful
Checks / check (push) Successful in -6m17s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m15s
2024-12-09 21:05:13 +02:00
9893b278e2
chore: bump to 0.1.3
Some checks failed
Checks / check (push) Failing after -5m57s
Build and Push Docker Image / Build and Push Image (push) Has been cancelled
2024-12-09 21:02:49 +02:00
c7b4373aef
feat: show day delta 2024-12-09 21:01:47 +02:00
13a1f32d8d
ci: migrate to forgejo
All checks were successful
Checks / check (push) Successful in -6m19s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m56s
2024-12-07 16:27:41 +02:00
08108ef71a
fix: dark mode on edit and add
All checks were successful
Checks / check (push) Successful in -2m56s
Build and Push Docker Image / Build and Push Image (push) Successful in -2m49s
2024-12-02 17:31:03 +02:00
bc77739425
chore: bump version to 0.1.2
All checks were successful
Checks / check (push) Successful in -2m56s
Build and Push Docker Image / Build and Push Image (push) Successful in -1m54s
2024-12-01 19:28:37 +02:00
eee0b548e9
Merge pull request 'feat: add dark mode' (#2) from dark-mode into main
All checks were successful
Checks / check (push) Successful in -2m55s
Build and Push Docker Image / Build and Push Image (push) Successful in -3m25s
Reviewed-on: #2
2024-12-01 19:20:45 +02:00
6f47a56653
feat: add dark mode
All checks were successful
Checks / check (pull_request) Successful in -2m56s
2024-12-01 18:48:39 +02:00
b98cb3e145
chore: add license
All checks were successful
Checks / check (push) Successful in -2m24s
Build and Push Docker Image / Build and Push Image (push) Successful in -3m19s
2024-12-01 17:10:36 +02:00
91c4ca2bc2
style: fix casing
All checks were successful
Checks / check (push) Successful in -2m51s
Build and Push Docker Image / Build and Push Image (push) Successful in -3m32s
2024-12-01 00:44:29 +02:00
c136d7eb07
ci: add tags to release workflow
All checks were successful
Checks / check (push) Successful in 1m30s
Build and Push Docker Image / Build and Push Image (push) Successful in 35s
2024-11-30 22:49:44 +02:00
cb4aba5860
chore: bump version to 0.1.1
All checks were successful
Checks / check (push) Successful in 1m31s
Build and Push Docker Image / Build and Push Image (push) Successful in 2m25s
2024-11-30 22:37:06 +02:00
4ec7fc5cf3
ci: add release workflow
All checks were successful
Checks / check (push) Successful in 1m35s
Build and Push Docker Image / Build and Push Image (push) Successful in 33s
2024-11-30 20:39:06 +02:00
32 changed files with 1412 additions and 148 deletions

View file

@ -19,7 +19,7 @@ jobs:
- name: Setup ssl - name: Setup ssl
run: wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.23_amd64.deb && sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2.23_amd64.deb run: wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.23_amd64.deb && sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2.23_amd64.deb
- uses: erlef/setup-beam@v1 - uses: https://github.com/erlef/setup-beam@v1
env: env:
ImageOS: ubuntu24 ImageOS: ubuntu24
with: with:

View file

@ -3,12 +3,12 @@ name: Build and Push Docker Image
on: on:
push: push:
branches: branches:
- main # Change this to your default branch if different - main
jobs: jobs:
build: build:
name: Build and Push Image name: Build and Push Image
runs-on: ubuntu-latest-root # This specifies the environment for the job runs-on: ubuntu-latest-root
steps: steps:
- name: Checkout Repository - name: Checkout Repository
@ -17,9 +17,9 @@ jobs:
- name: Login to Docker Registry - name: Login to Docker Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: git.vavakado.xyz # Replace with your Docker registry URL registry: git.vavakado.xyz
username: ${{ gitea.actor }} # The actor who triggered the action username: vavakado
password: ${{ secrets.REGISTRY_TOKEN }} # Your registry token secret password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build Docker Image - name: Build Docker Image
run: | run: |

View file

@ -0,0 +1,67 @@
name: Manual Release and Docker Image Build
on:
workflow_dispatch:
inputs:
version:
description: "Version for the release (e.g., v1.0.0)"
required: true
default: "v1.0.0"
type: string
jobs:
release:
name: Create Release and Build Docker Image
runs-on: ubuntu-latest-root
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: git.vavakado.xyz
username: vavakado
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Create Git Tag
run: |
git tag ${{ github.event.inputs.version }}
git push origin ${{ github.event.inputs.version }}
# - name: Create Release
# run: |
# curl -X POST -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
# -H "Content-Type: application/json" \
# -d '{
# "tag_name": "${{ github.event.inputs.version }}",
# "target_commitish": "main", # Change if your default branch is different
# "name": "${{ github.event.inputs.version }}",
# "body": "Release for version ${{ github.event.inputs.version }}",
# "draft": false,
# "prerelease": false
# }' \
# https://git.vavakado.xyz/api/v1/repos/${{ gitea.repository_owner }}/${{ gitea.repository }}/releases
# - name: Create Release
# uses: actions/forgejo-release@v2
# with:
# direction: upload
# tag: ${{ github.event.inputs.version }}
# title: ${{ github.event.inputs.version }}
# token: ${{ secrets.RELEASE_TOKEN }}
# release-notes-assistant: true
# release-dir: ./
- name: Build Docker Image
run: |
docker build -t git.vavakado.xyz/${{ gitea.repository_owner }}/exmr:${{ github.event.inputs.version }} .
docker tag git.vavakado.xyz/vavakado/exmr:${{ github.event.inputs.version }} git.vavakado.xyz/vavakado/exmr:latest
docker tag git.vavakado.xyz/vavakado/exmr:${{ github.event.inputs.version }} git.vavakado.xyz/vavakado/exmr:stable
- name: Push Docker Image
run: |
docker push git.vavakado.xyz/${{ gitea.repository_owner }}/exmr:${{ github.event.inputs.version }}
docker push git.vavakado.xyz/vavakado/exmr:latest
docker push git.vavakado.xyz/vavakado/exmr:stable

View file

@ -18,7 +18,7 @@ ARG DEBIAN_VERSION=bullseye-20241111-slim
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
FROM ${BUILDER_IMAGE} as builder FROM ${BUILDER_IMAGE} AS builder
# install build dependencies # install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git \ RUN apt-get update -y && apt-get install -y build-essential git \

8
LICENSE Normal file
View file

@ -0,0 +1,8 @@
Copyright 2024 Vladimir Rubin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -9,12 +9,8 @@ To start your Phoenix server:
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). ## TODO
## Learn more - [ ] Add tests
- [ ] Add a proper readme
- Official website: https://www.phoenixframework.org/ - [ ] Add a calendar view
- Guides: https://hexdocs.pm/phoenix/overview.html
- Docs: https://hexdocs.pm/phoenix
- Forum: https://elixirforum.com/c/phoenix-forum
- Source: https://github.com/phoenixframework/phoenix

View file

@ -16,29 +16,35 @@
// //
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. // Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html" import "phoenix_html";
// Establish Phoenix Socket and LiveView configuration. // Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix" import { Socket } from "phoenix";
import {LiveSocket} from "phoenix_live_view" import { LiveSocket } from "phoenix_live_view";
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar";
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") import darkModeHook from "../vendor/dark_mode";
let Hooks = {};
Hooks.DarkThemeToggle = darkModeHook;
let csrfToken = document
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, { let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500, longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken} params: { _csrf_token: csrfToken },
}) hooks: Hooks,
});
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300));
window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide());
// connect if there are any LiveViews on the page // connect if there are any LiveViews on the page
liveSocket.connect() liveSocket.connect();
// expose liveSocket on window for web console debug logs and latency simulation: // expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug() // >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim() // >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket window.liveSocket = liveSocket;

View file

@ -7,6 +7,7 @@ const path = require("path");
module.exports = { module.exports = {
content: ["./js/**/*.js", "../lib/exmr_web.ex", "../lib/exmr_web/**/*.*ex"], content: ["./js/**/*.js", "../lib/exmr_web.ex", "../lib/exmr_web/**/*.*ex"],
darkMode: "class",
theme: { theme: {
extend: { extend: {
colors: { colors: {

46
assets/vendor/dark_mode.js vendored Normal file
View file

@ -0,0 +1,46 @@
const localStorageKey = "theme";
const isDark = () => {
if (localStorage.getItem(localStorageKey) === "dark") return true;
if (localStorage.getItem(localStorageKey) === "light") return false;
return window.matchMedia("(prefers-color-scheme: dark)").matches;
};
const setupThemeToggle = () => {
toggleVisibility = (dark) => {
const themeToggleDarkIcon = document.getElementById(
"theme-toggle-dark-icon",
);
const themeToggleLightIcon = document.getElementById(
"theme-toggle-light-icon",
);
if (themeToggleDarkIcon == null || themeToggleLightIcon == null) return;
const show = dark ? themeToggleDarkIcon : themeToggleLightIcon;
const hide = dark ? themeToggleLightIcon : themeToggleDarkIcon;
show.classList.remove("hidden", "text-transparent");
hide.classList.add("hidden", "text-transparent");
if (dark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
try {
localStorage.setItem(localStorageKey, dark ? "dark" : "light");
} catch (_err) { }
};
toggleVisibility(isDark());
document
.getElementById("theme-toggle")
.addEventListener("click", function() {
toggleVisibility(!isDark());
});
};
const darkModeHook = {
mounted() {
setupThemeToggle();
},
updated() { },
};
export default darkModeHook;

View file

@ -48,9 +48,13 @@ defmodule ExmrWeb.CoreComponents do
phx-mounted={@show && show_modal(@id)} phx-mounted={@show && show_modal(@id)}
phx-remove={hide_modal(@id)} phx-remove={hide_modal(@id)}
data-cancel={JS.exec(@on_cancel, "phx-remove")} data-cancel={JS.exec(@on_cancel, "phx-remove")}
class="relative z-50 hidden" class="relative z-50 hidden bg-zinc-50/90 dark:bg-zinc-950"
> >
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" /> <div
id={"#{@id}-bg"}
class="bg-zinc-50/90 dark:bg-zinc-800/90 fixed inset-0 transition-opacity"
aria-hidden="true"
/>
<div <div
class="fixed inset-0 overflow-y-auto" class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"} aria-labelledby={"#{@id}-title"}
@ -66,7 +70,7 @@ defmodule ExmrWeb.CoreComponents do
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")} phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
phx-key="escape" phx-key="escape"
phx-click-away={JS.exec("data-cancel", to: "##{@id}")} phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition" class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white dark:bg-zinc-950 p-14 shadow-lg ring-1 transition"
> >
<div class="absolute top-6 right-5"> <div class="absolute top-6 right-5">
<button <button
@ -79,7 +83,7 @@ defmodule ExmrWeb.CoreComponents do
</button> </button>
</div> </div>
<div id={"#{@id}-content"}> <div id={"#{@id}-content"}>
<%= render_slot(@inner_block) %> {render_slot(@inner_block)}
</div> </div>
</.focus_wrap> </.focus_wrap>
</div> </div>
@ -124,9 +128,9 @@ defmodule ExmrWeb.CoreComponents do
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6"> <p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" /> <.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" /> <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
<%= @title %> {@title}
</p> </p>
<p class="mt-2 text-sm leading-5"><%= msg %></p> <p class="mt-2 text-sm leading-5">{msg}</p>
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}> <button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" /> <.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
</button> </button>
@ -157,7 +161,7 @@ defmodule ExmrWeb.CoreComponents do
phx-connected={hide("#client-error")} phx-connected={hide("#client-error")}
hidden hidden
> >
<%= gettext("Attempting to reconnect") %> {gettext("Attempting to reconnect")}
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" /> <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash> </.flash>
@ -169,7 +173,7 @@ defmodule ExmrWeb.CoreComponents do
phx-connected={hide("#server-error")} phx-connected={hide("#server-error")}
hidden hidden
> >
<%= gettext("Hang in there while we get back on track") %> {gettext("Hang in there while we get back on track")}
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" /> <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash> </.flash>
</div> </div>
@ -202,10 +206,13 @@ defmodule ExmrWeb.CoreComponents do
def simple_form(assigns) do def simple_form(assigns) do
~H""" ~H"""
<.form :let={f} for={@for} as={@as} {@rest}> <.form :let={f} for={@for} as={@as} {@rest}>
<div class="mt-10 space-y-8 bg-white"> <div class="mt-10 space-y-8 bg-white dark:bg-zinc-950">
<%= render_slot(@inner_block, f) %> {render_slot(@inner_block, f)}
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6"> <div
<%= render_slot(action, f) %> :for={action <- @actions}
class="mt-2 flex items-center justify-between gap-6 dark:text-zinc-100"
>
{render_slot(action, f)}
</div> </div>
</div> </div>
</.form> </.form>
@ -232,12 +239,12 @@ defmodule ExmrWeb.CoreComponents do
type={@type} type={@type}
class={[ class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3", "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80", "text-sm font-semibold leading-6",
@class @class
]} ]}
{@rest} {@rest}
> >
<%= render_slot(@inner_block) %> {render_slot(@inner_block)}
</button> </button>
""" """
end end
@ -310,7 +317,7 @@ defmodule ExmrWeb.CoreComponents do
~H""" ~H"""
<div> <div>
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600"> <label class="flex items-center gap-4 text-sm leading-6 text-zinc-600 dark:text-zinc-200">
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} /> <input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input <input
type="checkbox" type="checkbox"
@ -318,12 +325,12 @@ defmodule ExmrWeb.CoreComponents do
name={@name} name={@name}
value="true" value="true"
checked={@checked} checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0" class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
{@rest} {@rest}
/> />
<%= @label %> {@label}
</label> </label>
<.error :for={msg <- @errors}><%= msg %></.error> <.error :for={msg <- @errors}>{msg}</.error>
</div> </div>
""" """
end end
@ -331,7 +338,7 @@ defmodule ExmrWeb.CoreComponents do
def input(%{type: "select"} = assigns) do def input(%{type: "select"} = assigns) do
~H""" ~H"""
<div> <div>
<.label for={@id}><%= @label %></.label> <.label for={@id}>{@label}</.label>
<select <select
id={@id} id={@id}
name={@name} name={@name}
@ -339,10 +346,10 @@ defmodule ExmrWeb.CoreComponents do
multiple={@multiple} multiple={@multiple}
{@rest} {@rest}
> >
<option :if={@prompt} value=""><%= @prompt %></option> <option :if={@prompt} value="">{@prompt}</option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %> {Phoenix.HTML.Form.options_for_select(@options, @value)}
</select> </select>
<.error :for={msg <- @errors}><%= msg %></.error> <.error :for={msg <- @errors}>{msg}</.error>
</div> </div>
""" """
end end
@ -350,7 +357,7 @@ defmodule ExmrWeb.CoreComponents do
def input(%{type: "textarea"} = assigns) do def input(%{type: "textarea"} = assigns) do
~H""" ~H"""
<div> <div>
<.label for={@id}><%= @label %></.label> <.label for={@id}>{@label}</.label>
<textarea <textarea
id={@id} id={@id}
name={@name} name={@name}
@ -361,7 +368,7 @@ defmodule ExmrWeb.CoreComponents do
]} ]}
{@rest} {@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea> ><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.error :for={msg <- @errors}><%= msg %></.error> <.error :for={msg <- @errors}>{msg}</.error>
</div> </div>
""" """
end end
@ -370,7 +377,7 @@ defmodule ExmrWeb.CoreComponents do
def input(assigns) do def input(assigns) do
~H""" ~H"""
<div> <div>
<.label for={@id}><%= @label %></.label> <.label for={@id}>{@label}</.label>
<input <input
type={@type} type={@type}
name={@name} name={@name}
@ -383,7 +390,7 @@ defmodule ExmrWeb.CoreComponents do
]} ]}
{@rest} {@rest}
/> />
<.error :for={msg <- @errors}><%= msg %></.error> <.error :for={msg <- @errors}>{msg}</.error>
</div> </div>
""" """
end end
@ -396,8 +403,8 @@ defmodule ExmrWeb.CoreComponents do
def label(assigns) do def label(assigns) do
~H""" ~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800"> <label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800 dark:text-zinc-200">
<%= render_slot(@inner_block) %> {render_slot(@inner_block)}
</label> </label>
""" """
end end
@ -411,7 +418,7 @@ defmodule ExmrWeb.CoreComponents do
~H""" ~H"""
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600"> <p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<%= render_slot(@inner_block) %> {render_slot(@inner_block)}
</p> </p>
""" """
end end
@ -429,14 +436,14 @@ defmodule ExmrWeb.CoreComponents do
~H""" ~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}> <header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<div> <div>
<h1 class="text-lg font-semibold leading-8 text-zinc-800"> <h1 class="text-lg font-semibold leading-none text-zinc-800 dark:text-zinc-200">
<%= render_slot(@inner_block) %> {render_slot(@inner_block)}
</h1> </h1>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600"> <p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600 dark:text-zinc-100">
<%= render_slot(@subtitle) %> {render_slot(@subtitle)}
</p> </p>
</div> </div>
<div class="flex-none"><%= render_slot(@actions) %></div> <div class="flex-none">{render_slot(@actions)}</div>
</header> </header>
""" """
end end
@ -477,9 +484,9 @@ defmodule ExmrWeb.CoreComponents do
<table class="w-[40rem] mt-11 sm:w-full"> <table class="w-[40rem] mt-11 sm:w-full">
<thead class="text-sm text-left leading-6 text-zinc-500"> <thead class="text-sm text-left leading-6 text-zinc-500">
<tr> <tr>
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th> <th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal">{col[:label]}</th>
<th :if={@action != []} class="relative p-0 pb-4"> <th :if={@action != []} class="relative p-0 pb-4">
<span class="sr-only"><%= gettext("Actions") %></span> <span class="sr-only">{gettext("Actions")}</span>
</th> </th>
</tr> </tr>
</thead> </thead>
@ -497,7 +504,7 @@ defmodule ExmrWeb.CoreComponents do
<div class="block py-4 pr-6"> <div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" /> <span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}> <span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
<%= render_slot(col, @row_item.(row)) %> {render_slot(col, @row_item.(row))}
</span> </span>
</div> </div>
</td> </td>
@ -508,7 +515,7 @@ defmodule ExmrWeb.CoreComponents do
:for={action <- @action} :for={action <- @action}
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700" class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
> >
<%= render_slot(action, @row_item.(row)) %> {render_slot(action, @row_item.(row))}
</span> </span>
</div> </div>
</td> </td>
@ -538,8 +545,8 @@ defmodule ExmrWeb.CoreComponents do
<div class="mt-14"> <div class="mt-14">
<dl class="-my-4 divide-y divide-zinc-100"> <dl class="-my-4 divide-y divide-zinc-100">
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8"> <div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
<dt class="w-1/4 flex-none text-zinc-500"><%= item.title %></dt> <dt class="w-1/4 flex-none text-zinc-500">{item.title}</dt>
<dd class="text-zinc-700"><%= render_slot(item) %></dd> <dd class="text-zinc-700">{render_slot(item)}</dd>
</div> </div>
</dl> </dl>
</div> </div>
@ -564,7 +571,7 @@ defmodule ExmrWeb.CoreComponents do
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
> >
<.icon name="hero-arrow-left-solid" class="h-3 w-3" /> <.icon name="hero-arrow-left-solid" class="h-3 w-3" />
<%= render_slot(@inner_block) %> {render_slot(@inner_block)}
</.link> </.link>
</div> </div>
""" """

View file

@ -0,0 +1,57 @@
defmodule DarkMode do
@moduledoc """
A component to toggle dark mode.
"""
use Phoenix.Component
def button(assigns) do
~H"""
<button
id="theme-toggle"
type="button"
phx-update="ignore"
phx-hook="DarkThemeToggle"
class="text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded-lg text-sm p-2"
>
<svg
id="theme-toggle-dark-icon"
class="w-5 h-5 text-transparent hidden"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg
id="theme-toggle-light-icon"
class="w-5 h-5 text-transparent"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fill-rule="evenodd"
clip-rule="evenodd"
>
</path>
</svg>
</button>
<script>
// Toggle early based on <html class="dark">
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggleDarkIcon != null && themeToggleLightIcon != null) {
let dark = document.documentElement.classList.contains('dark');
const show = dark ? themeToggleDarkIcon : themeToggleLightIcon
const hide = dark ? themeToggleLightIcon : themeToggleDarkIcon
show.classList.remove('hidden', 'text-transparent');
hide.classList.add('hidden', 'text-transparent');
}
</script>
"""
end
end

View file

@ -5,7 +5,7 @@
<img src={~p"/images/logo.svg"} width="36" /> <img src={~p"/images/logo.svg"} width="36" />
</a> </a>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6"> <p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
v<%= Application.spec(:exmr, :vsn) %> v{Application.spec(:exmr, :vsn)}
</p> </p>
</div> </div>
</div> </div>
@ -13,6 +13,6 @@
<main class="px-4 py-20 sm:px-6 lg:px-8"> <main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} /> <.flash_group flash={@flash} />
<%= @inner_content %> {@inner_content}
</div> </div>
</main> </main>

View file

@ -4,55 +4,63 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} /> <meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Phoenix Framework"> <.live_title>
<%= assigns[:page_title] || "Exmr" %> {assigns[:page_title] || "ExMR"}
</.live_title> </.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} /> <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script>
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark')
}
</script>
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}> <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script> </script>
</head> </head>
<body class="bg-white"> <body class="bg-white dark:bg-zinc-950 dark:text-zinc-100">
<ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end"> <ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<DarkMode.button />
<%= if @current_user do %> <%= if @current_user do %>
<li class="text-[0.8125rem] leading-6 text-zinc-900"> <li class="text-[0.8125rem] leading-6 text-zinc-900 dark:text-zinc-300">
<%= @current_user.email %> {@current_user.email}
</li> </li>
<li> <li>
<.link <.link
href={~p"/users/settings"} href={~p"/users/settings"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700" class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
> >
Settings {gettext("Settings")}
</.link> </.link>
</li> </li>
<li> <li>
<.link <.link
href={~p"/users/log_out"} href={~p"/users/log_out"}
method="delete" method="delete"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700" class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
> >
Log out {gettext("Log out")}
</.link> </.link>
</li> </li>
<% else %> <% else %>
<li> <li>
<.link <.link
href={~p"/users/register"} href={~p"/users/register"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700" class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
> >
Register {gettext("Register")}
</.link> </.link>
</li> </li>
<li> <li>
<.link <.link
href={~p"/users/log_in"} href={~p"/users/log_in"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700" class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
> >
Log in {gettext("Log in")}
</.link> </.link>
</li> </li>
<% end %> <% end %>
</ul> </ul>
<%= @inner_content %> {@inner_content}
</body> </body>
</html> </html>

View file

@ -44,25 +44,25 @@
<h1 class="mt-10 flex items-center text-sm font-semibold leading-6 text-purple-400"> <h1 class="mt-10 flex items-center text-sm font-semibold leading-6 text-purple-400">
ExMR ExMR
<small class="bg-brand/5 text-[0.8125rem] ml-3 rounded-full px-2 font-medium leading-6 text-purple-400"> <small class="bg-brand/5 text-[0.8125rem] ml-3 rounded-full px-2 font-medium leading-6 text-purple-400">
v<%= Application.spec(:exmr, :vsn) %> v{Application.spec(:exmr, :vsn)}
</small> </small>
</h1> </h1>
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900 text-balance"> <p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900 dark:text-zinc-200/90 text-balance">
hell yeah {gettext("A simple, modern, and fast exam management system.")}
</p> </p>
<p class="mt-4 text-base leading-7 text-zinc-600"> <p class="mt-4 text-base leading-7 text-zinc-600 dark:text-zinc-300">
y'all really though i'm gonna be serious? lol {gettext("Built using Phoenix LiveView, Ecto, and TailwindCSS by @vavakado")}
</p> </p>
<div class="flex"> <div class="flex">
<div class="w-full sm:w-auto"> <div class="w-full sm:w-auto">
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3"> <div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
<a <a
href="https://github.com/phoenixframework/phoenix" href="https://git.vavakado.xyz/vavakado/exmr"
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6" class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
> >
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105"> <span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 dark:bg-zinc-800 dark:group-hover:bg-zinc-900/95 sm:group-hover:scale-105">
</span> </span>
<span class="relative flex items-center gap-4 sm:flex-col"> <span class="relative flex items-center gap-4 sm:flex-col dark:text-white">
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6"> <svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
<path <path
fill-rule="evenodd" fill-rule="evenodd"
@ -79,9 +79,9 @@
href="https://mas.to/@vavakado" href="https://mas.to/@vavakado"
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6" class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
> >
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105"> <span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105 dark:bg-zinc-800 dark:group-hover:bg-zinc-900/95">
</span> </span>
<span class="relative flex items-center gap-4 sm:flex-col"> <span class="relative flex items-center gap-4 sm:flex-col dark:text-white">
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6"> <svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a4 4 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522q0-1.288.66-2.046c.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764q.662.757.661 2.046z" /> <path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a4 4 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522q0-1.288.66-2.046c.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764q.662.757.661 2.046z" />
</svg> </svg>
@ -93,7 +93,7 @@
</div> </div>
<div class="text-center py-8 font-bold text-3xl text-red-600"> <div class="text-center py-8 font-bold text-3xl text-red-600">
<.link navigate={~p"/exams"} class="underline hover:text-red-400 transition-all ease-in-out"> <.link navigate={~p"/exams"} class="underline hover:text-red-400 transition-all ease-in-out">
exams {gettext("Exams")}
</.link> </.link>
</div> </div>
</div> </div>

7
lib/exmr_web/helpers.ex Normal file
View file

@ -0,0 +1,7 @@
defmodule ExmrWeb.LiveHelpers do
def on_mount(:default, _params, session, socket) do
locale = session["locale"] || "en"
Gettext.put_locale(locale)
{:cont, socket}
end
end

View file

@ -8,8 +8,10 @@ defmodule ExmrWeb.ExamLive.FormComponent do
~H""" ~H"""
<div> <div>
<.header> <.header>
<%= @title %> {@title}
<:subtitle>Use this form to manage exam records in your database.</:subtitle> <:subtitle>
{gettext("Use this form to manage exam records in your database.")}
</:subtitle>
</.header> </.header>
<.simple_form <.simple_form
@ -19,11 +21,11 @@ defmodule ExmrWeb.ExamLive.FormComponent do
phx-change="validate" phx-change="validate"
phx-submit="save" phx-submit="save"
> >
<.input field={@form[:subject]} type="text" label="Subject" /> <.input field={@form[:subject]} type="text" label={gettext("Subject")} />
<.input field={@form[:description]} type="text" label="Description" /> <.input field={@form[:description]} type="text" label={gettext("Description")} />
<.input field={@form[:date]} type="date" label="Date" /> <.input field={@form[:date]} type="date" label={gettext("Date")} />
<:actions> <:actions>
<.button phx-disable-with="Saving...">Save Exam</.button> <.button phx-disable-with={gettext("Saving...")}>{gettext("Save Exam")}</.button>
</:actions> </:actions>
</.simple_form> </.simple_form>
</div> </div>

View file

@ -12,6 +12,8 @@ defmodule ExmrWeb.ExamLive.Index do
|> assign(:sort_by, "date") |> assign(:sort_by, "date")
|> assign(:live_action, :index) |> assign(:live_action, :index)
|> assign(exam: %{}) |> assign(exam: %{})
|> assign(events: Enum.group_by(Exams.list_exams(), & &1.date))
|> assign(current_month: Date.utc_today())
{:ok, socket} {:ok, socket}
end end
@ -29,7 +31,7 @@ defmodule ExmrWeb.ExamLive.Index do
defp apply_action(socket, :new, _params) do defp apply_action(socket, :new, _params) do
socket socket
|> assign(:page_title, "New Exam") |> assign(:page_title, gettext("New Exam"))
|> assign(:exam, %Exam{}) |> assign(:exam, %Exam{})
end end
@ -44,6 +46,7 @@ defmodule ExmrWeb.ExamLive.Index do
socket = socket =
socket socket
|> assign(:exams, [exam | socket.assigns.exams]) |> assign(:exams, [exam | socket.assigns.exams])
|> assign(events: Enum.group_by(Exams.list_exams(), & &1.date))
{:noreply, socket} {:noreply, socket}
end end
@ -58,6 +61,7 @@ defmodule ExmrWeb.ExamLive.Index do
|> update(:exams, fn exams -> |> update(:exams, fn exams ->
Enum.reject(exams, fn l -> l.id == exam.id end) Enum.reject(exams, fn l -> l.id == exam.id end)
end) end)
|> assign(events: Enum.group_by(Exams.list_exams(), & &1.date))
{:noreply, socket} {:noreply, socket}
end end

View file

@ -1,14 +1,27 @@
<.header> <.header>
Listing Exams {gettext("Listing Exams")}
<div> <div>
<button phx-click="sort" phx-value-by="subject" class="font-light">Sort by Subject</button> <button
<a>|</a> phx-click="sort"
phx-value-by="subject"
<button phx-click="sort" phx-value-by="date" class="font-light">Sort by Date</button> class="font-light text-xs md:text-sm lg:text-base"
>
{gettext("Sort by Subject")}
</button>
<a class="invisible lg:visible">|</a>
<button
phx-click="sort"
phx-value-by="date"
class="font-light text-xs md:text-sm lg:text-base"
>
{gettext("Sort by Date")}
</button>
</div> </div>
<:actions> <:actions>
<.link patch={~p"/exams/new"}> <.link patch={~p"/exams/new"}>
<.button>New Exam</.button> <.button class="dark:bg-sky-300 text-white dark:text-black/80 dark:hover:bg-sky-400">
{gettext("New Exam")}
</.button>
</.link> </.link>
</:actions> </:actions>
</.header> </.header>
@ -16,22 +29,32 @@
<div class="divide-y"> <div class="divide-y">
<div :for={exam <- @exams} class="py-2 flex gap-2"> <div :for={exam <- @exams} class="py-2 flex gap-2">
<div class="grow"> <div class="grow">
<div class="font-bold"><%= exam.subject %></div> <div class="font-bold text-xs md:text-sm lg:text-base">{exam.subject}</div>
<div class="font-sm"><%= exam.date %></div> <div class="text-xs md:text-sm lg:text-base">
{exam.date} |
<b class="text-xs md:text-sm lg:text-base">
{case Date.diff(exam.date, Date.utc_today()) do
0 -> gettext("Today")
1 -> gettext("Tommorow")
x when x > 1 -> ngettext("Tomorrow", "%{count} days left", x)
x when x < 0 -> ngettext("Yesterday", "%{count} days passed", x * -1)
end}
</b>
</div>
</div> </div>
<button <button
phx-click="edit" phx-click="edit"
phx-value-id={exam.id} phx-value-id={exam.id}
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-3 rounded-md" class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-1 text-xs md:text-sm lg:py-2 lg:px-3 rounded-md"
> >
Edit {gettext("Edit")}
</button> </button>
<button <button
phx-click="remove" phx-click="remove"
phx-value-id={exam.id} phx-value-id={exam.id}
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-3 rounded-md" class="bg-red-500 hover:bg-red-700 text-white font-bold p-1 text-xs md:text-sm lg:py-2 lg:px-3 rounded-md"
> >
Remove {gettext("Remove")}
</button> </button>
</div> </div>
</div> </div>
@ -46,3 +69,53 @@
patch={~p"/exams"} patch={~p"/exams"}
/> />
</.modal> </.modal>
<div>
<!-- Navigation -->
<!-- <div class="flex justify-between mb-4"> -->
<!-- <button phx-click="previous_month" class="px-2 py-1 bg-blue-200">Previous</button> -->
<!-- <h2>{@current_month |> Date.to_string() |> String.replace("-", " ")}</h2> -->
<!-- <button phx-click="next_month" class="px-2 py-1 bg-blue-200">Next</button> -->
<!-- </div> -->
<!-- Calendar -->
<div class="grid grid-cols-7 gap-1" id="calendar-container" phx-update="replace">
<!-- Weekdays -->
<%= for weekday <- ~w(#{gettext("Sun")} #{gettext("Mon")} #{gettext("Tue")} #{gettext("Wed")} #{gettext("Thu")}
#{gettext("Fri")} #{gettext("Sat")}) do %>
<div class="font-bold text-center">{weekday}</div>
<% end %>
<!-- Empty spaces for previous month -->
<%= for _ <- 1..(Date.day_of_week(Date.new!(@current_month.year, @current_month.month, 1))) do %>
<div></div>
<% end %>
<!-- Days of the month -->
<%= for day <- 1..Date.days_in_month(@current_month) do %>
<% {:ok, date} = Date.new(@current_month.year, @current_month.month, day) %>
<div
id={"day-#{Date.to_string(date)}"}
class={[
"border rounded p-0.5 md:p-2 lg:p-3 w-full h-12 md:h-20 hover:scale-125 transition-transform ease-in-out duration-100",
@current_month == date &&
"bg-zinc-200 dark:bg-zinc-800 border-green-500 dark:border-green-300",
@current_month != date &&
"bg-white dark:bg-zinc-950 dark:border-zinc-400 "
]}
>
<div class="text-xs md:text-sm lg:text-base font-bold">{day}</div>
<!-- Render events for the current day -->
<%= if Map.has_key?(@events, date) do %>
<ul class="text-xs">
<%= for event <- @events[date] do %>
<li id={"event-#{event.id}"}>{event.subject}</li>
<% end %>
</ul>
<% end %>
</div>
<% end %>
</div>
</div>

View file

@ -1,6 +1,6 @@
<.header> <.header>
exam №<%= @exam.id %> exam №{@exam.id}
<:subtitle>очередная контрольная работа по <strong><%= @exam.subject %></strong></:subtitle> <:subtitle>очередная контрольная работа по <strong>{@exam.subject}</strong></:subtitle>
<:actions> <:actions>
<.link patch={~p"/exams/#{@exam}/show/edit"} phx-click={JS.push_focus()}> <.link patch={~p"/exams/#{@exam}/show/edit"} phx-click={JS.push_focus()}>
<.button>изменить</.button> <.button>изменить</.button>
@ -9,8 +9,8 @@
</.header> </.header>
<.list> <.list>
<:item title="Description"><%= @exam.description %></:item> <:item title="Description">{@exam.description}</:item>
<:item title="Date"><%= @exam.date %></:item> <:item title="Date">{@exam.date}</:item>
</.list> </.list>
<.back navigate={~p"/exams"}>Back to exams</.back> <.back navigate={~p"/exams"}>Back to exams</.back>

View file

@ -14,7 +14,7 @@ defmodule ExmrWeb.UserForgotPasswordLive do
<.simple_form for={@form} id="reset_password_form" phx-submit="send_email"> <.simple_form for={@form} id="reset_password_form" phx-submit="send_email">
<.input field={@form[:email]} type="email" placeholder="Email" required /> <.input field={@form[:email]} type="email" placeholder="Email" required />
<:actions> <:actions>
<.button phx-disable-with="Sending..." class="w-full"> <.button phx-disable-with="Sending..." class="w-full dark:text-black dark:bg-red-600">
Send password reset instructions Send password reset instructions
</.button> </.button>
</:actions> </:actions>

View file

@ -26,7 +26,7 @@ defmodule ExmrWeb.UserLoginLive do
</.link> </.link>
</:actions> </:actions>
<:actions> <:actions>
<.button phx-disable-with="Logging in..." class="w-full"> <.button phx-disable-with="Logging in..." class="w-full dark:bg-teal-600">
Log in <span aria-hidden="true"></span> Log in <span aria-hidden="true"></span>
</.button> </.button>
</:actions> </:actions>

View file

@ -6,7 +6,7 @@ defmodule ExmrWeb.UserRegistrationLive do
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div class="mx-auto max-w-sm"> <div class="mx-auto max-w-sm dark:bg-zinc-950">
<.header class="text-center"> <.header class="text-center">
Register for an account Register for an account
<:subtitle> <:subtitle>
@ -35,7 +35,9 @@ defmodule ExmrWeb.UserRegistrationLive do
<.input field={@form[:password]} type="password" label="Password" required /> <.input field={@form[:password]} type="password" label="Password" required />
<:actions> <:actions>
<.button phx-disable-with="Creating account..." class="w-full">Create an account</.button> <.button phx-disable-with="Creating account..." class="w-full dark:bg-green-800">
Create an account
</.button>
</:actions> </:actions>
</.simple_form> </.simple_form>
</div> </div>

View file

@ -6,8 +6,8 @@ defmodule ExmrWeb.UserSettingsLive do
def render(assigns) do def render(assigns) do
~H""" ~H"""
<.header class="text-center"> <.header class="text-center">
Account Settings {gettext("Account Settings")}
<:subtitle>Manage your account email address and password settings</:subtitle> <:subtitle>{gettext("Manage your account email address and password settings")}</:subtitle>
</.header> </.header>
<div class="space-y-12 divide-y"> <div class="space-y-12 divide-y">
@ -18,18 +18,20 @@ defmodule ExmrWeb.UserSettingsLive do
phx-submit="update_email" phx-submit="update_email"
phx-change="validate_email" phx-change="validate_email"
> >
<.input field={@email_form[:email]} type="email" label="Email" required /> <.input field={@email_form[:email]} type="email" label={gettext("Email")} required />
<.input <.input
field={@email_form[:current_password]} field={@email_form[:current_password]}
name="current_password" name="current_password"
id="current_password_for_email" id="current_password_for_email"
type="password" type="password"
label="Current password" label={gettext("Current password")}
value={@email_form_current_password} value={@email_form_current_password}
required required
/> />
<:actions> <:actions>
<.button phx-disable-with="Changing...">Change Email</.button> <.button phx-disable-with={gettext("Changing...")}>
{gettext("Change Email")}
</.button>
</:actions> </:actions>
</.simple_form> </.simple_form>
</div> </div>
@ -49,23 +51,30 @@ defmodule ExmrWeb.UserSettingsLive do
id="hidden_user_email" id="hidden_user_email"
value={@current_email} value={@current_email}
/> />
<.input field={@password_form[:password]} type="password" label="New password" required /> <.input
field={@password_form[:password]}
type="password"
label={gettext("New password")}
required
/>
<.input <.input
field={@password_form[:password_confirmation]} field={@password_form[:password_confirmation]}
type="password" type="password"
label="Confirm new password" label={gettext("Confirm new password")}
/> />
<.input <.input
field={@password_form[:current_password]} field={@password_form[:current_password]}
name="current_password" name="current_password"
type="password" type="password"
label="Current password" label={gettext("Current password")}
id="current_password_for_password" id="current_password_for_password"
value={@current_password} value={@current_password}
required required
/> />
<:actions> <:actions>
<.button phx-disable-with="Changing...">Change Password</.button> <.button phx-disable-with={gettext("Changing...")}>
{gettext("Change Password")}
</.button>
</:actions> </:actions>
</.simple_form> </.simple_form>
</div> </div>
@ -77,10 +86,10 @@ defmodule ExmrWeb.UserSettingsLive do
socket = socket =
case Users.update_user_email(socket.assigns.current_user, token) do case Users.update_user_email(socket.assigns.current_user, token) do
:ok -> :ok ->
put_flash(socket, :info, "Email changed successfully.") put_flash(socket, :info, gettext("Email changed successfully."))
:error -> :error ->
put_flash(socket, :error, "Email change link is invalid or it has expired.") put_flash(socket, :error, gettext("Email change link is invalid or it has expired."))
end end
{:ok, push_navigate(socket, to: ~p"/users/settings")} {:ok, push_navigate(socket, to: ~p"/users/settings")}
@ -127,7 +136,7 @@ defmodule ExmrWeb.UserSettingsLive do
&url(~p"/users/settings/confirm_email/#{&1}") &url(~p"/users/settings/confirm_email/#{&1}")
) )
info = "A link to confirm your email change has been sent to the new address." info = gettext("A link to confirm your email change has been sent to the new address.")
{:noreply, socket |> put_flash(:info, info) |> assign(email_form_current_password: nil)} {:noreply, socket |> put_flash(:info, info) |> assign(email_form_current_password: nil)}
{:error, changeset} -> {:error, changeset} ->

View file

@ -0,0 +1,67 @@
defmodule ExmrWeb.Plugs.Locale do
import Plug.Conn
def init(_opts), do: nil
def call(conn, _opts) do
accepted_languages = extract_accept_language(conn)
known_locales = Gettext.known_locales(ExmrWeb.Gettext)
accepted_languages =
known_locales --
known_locales -- accepted_languages
case accepted_languages do
[locale | _] ->
Gettext.put_locale(ExmrWeb.Gettext, locale)
conn
|> put_session(:locale, locale)
_ ->
conn
end
end
# Copied from
# https://raw.githubusercontent.com/smeevil/set_locale/fd35624e25d79d61e70742e42ade955e5ff857b8/lib/headers.ex
def extract_accept_language(conn) do
case Plug.Conn.get_req_header(conn, "accept-language") do
[value | _] ->
value
|> String.split(",")
|> Enum.map(&parse_language_option/1)
|> Enum.sort(&(&1.quality > &2.quality))
|> Enum.map(& &1.tag)
|> Enum.reject(&is_nil/1)
|> ensure_language_fallbacks()
_ ->
[]
end
end
defp parse_language_option(string) do
captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string)
quality =
case Float.parse(captures["quality"] || "1.0") do
{val, _} -> val
_ -> 1.0
end
%{tag: captures["tag"], quality: quality}
end
defp ensure_language_fallbacks(tags) do
Enum.flat_map(tags, fn tag ->
case String.split(tag, "-") do
[language, _country_variant] ->
if Enum.member?(tags, language), do: [tag], else: [tag, language]
[_language] ->
[tag]
end
end)
end
end

View file

@ -12,6 +12,7 @@ defmodule ExmrWeb.Router do
plug :put_root_layout, html: {ExmrWeb.Layouts, :root} plug :put_root_layout, html: {ExmrWeb.Layouts, :root}
plug :protect_from_forgery plug :protect_from_forgery
plug :put_secure_browser_headers plug :put_secure_browser_headers
plug ExmrWeb.Plugs.Locale
plug :fetch_current_user plug :fetch_current_user
end end
@ -53,7 +54,7 @@ defmodule ExmrWeb.Router do
pipe_through [:browser, :redirect_if_user_is_authenticated] pipe_through [:browser, :redirect_if_user_is_authenticated]
live_session :redirect_if_user_is_authenticated, live_session :redirect_if_user_is_authenticated,
on_mount: [{ExmrWeb.UserAuth, :redirect_if_user_is_authenticated}] do on_mount: [{ExmrWeb.UserAuth, :redirect_if_user_is_authenticated}, ExmrWeb.LiveHelpers] do
if Exmr.enable_registration() != "false" do if Exmr.enable_registration() != "false" do
live "/users/register", UserRegistrationLive, :new live "/users/register", UserRegistrationLive, :new
end end
@ -70,7 +71,7 @@ defmodule ExmrWeb.Router do
pipe_through [:browser, :require_authenticated_user] pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user, live_session :require_authenticated_user,
on_mount: [{ExmrWeb.UserAuth, :ensure_authenticated}] do on_mount: [{ExmrWeb.UserAuth, :ensure_authenticated}, ExmrWeb.LiveHelpers] do
if Exmr.enable_registration() == "false" do if Exmr.enable_registration() == "false" do
live "/users/register", UserRegistrationLive, :new live "/users/register", UserRegistrationLive, :new
end end
@ -93,7 +94,7 @@ defmodule ExmrWeb.Router do
delete "/users/log_out", UserSessionController, :delete delete "/users/log_out", UserSessionController, :delete
live_session :current_user, live_session :current_user,
on_mount: [{ExmrWeb.UserAuth, :mount_current_user}] do on_mount: [{ExmrWeb.UserAuth, :mount_current_user}, ExmrWeb.LiveHelpers] do
live "/users/confirm/:token", UserConfirmationLive, :edit live "/users/confirm/:token", UserConfirmationLive, :edit
live "/users/confirm", UserConfirmationInstructionsLive, :new live "/users/confirm", UserConfirmationInstructionsLive, :new
end end

View file

@ -4,7 +4,7 @@ defmodule Exmr.MixProject do
def project do def project do
[ [
app: :exmr, app: :exmr,
version: "0.1.0", version: "0.2.2",
elixir: "~> 1.14", elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
@ -40,8 +40,7 @@ defmodule Exmr.MixProject do
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:phoenix_html, "~> 4.1"}, {:phoenix_html, "~> 4.1"},
{:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_reload, "~> 1.2", only: :dev},
# TODO bump on release to {:phoenix_live_view, "~> 1.0.0"}, {:phoenix_live_view, "~> 1.0.0"},
{:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
{:floki, ">= 0.30.0", only: :test}, {:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.8.3"}, {:phoenix_live_dashboard, "~> 0.8.3"},
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},

View file

@ -1,5 +1,5 @@
%{ %{
"bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, "bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
@ -15,21 +15,21 @@
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "hpax": {:hex, :hpax, "1.0.1", "c857057f89e8bd71d97d9042e009df2a42705d6d690d54eca84c8b29af0787b0", [:mix], [], "hexpm", "4e2d5a4f76ae1e3048f35ae7adb1641c36265510a2d4638157fbcb53dda38445"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, "phoenix": {:hex, :phoenix, "1.7.18", "5310c21443514be44ed93c422e15870aef254cf1b3619e4f91538e7529d2b2e4", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1797fcc82108442a66f2c77a643a62980f342bfeb63d6c9a515ab8294870004e"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
"phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.5", "d5f44d7dbd7cfacaa617b70c5a14b2b598d6f93b9caa8e350c51d56cd4350a9b", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1d73920515554d7d6c548aee0bf10a4780568b029d042eccb336db29ea0dad70"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.5", "d5f44d7dbd7cfacaa617b70c5a14b2b598d6f93b9caa8e350c51d56cd4350a9b", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1d73920515554d7d6c548aee0bf10a4780568b029d042eccb336db29ea0dad70"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.7", "d2abca526422adea88896769529addb6443390b1d4f1ff9cbe694312d8875fb2", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b82a4575f6f3eb5b97922ec6874b0c52b3ca0cc5dcb4b14ddc478cbfa135dd01"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0", "3a10dfce8f87b2ad4dc65de0732fc2a11e670b2779a19e8d3281f4619a85bce4", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "254caef0028765965ca6bd104cc7d68dcc7d57cc42912bef92f6b03047251d99"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
@ -40,7 +40,7 @@
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, "thousand_island": {:hex, :thousand_island, "1.3.7", "1da7598c0f4f5f50562c097a3f8af308ded48cd35139f0e6f17d9443e4d0c9c5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0139335079953de41d381a6134d8b618d53d084f558c734f2662d1a72818dd12"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
} }

270
priv/gettext/default.pot Normal file
View file

@ -0,0 +1,270 @@
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new messages manually only if they're dynamic
## messages that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here has no
## effect: edit them in PO (.po) files instead.
#
msgid ""
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:51
#, elixir-autogen, elixir-format
msgid "A simple, modern, and fast exam management system."
msgstr ""
#: lib/exmr_web/components/core_components.ex:489
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/exmr_web/components/core_components.ex:164
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:54
#, elixir-autogen, elixir-format
msgid "Built using Phoenix LiveView, Ecto, and TailwindCSS by @vavakado"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:50
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
#: lib/exmr_web/components/core_components.ex:155
#, elixir-autogen, elixir-format
msgid "Error!"
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:96
#, elixir-autogen, elixir-format
msgid "Exams"
msgstr ""
#: lib/exmr_web/components/core_components.ex:176
#, elixir-autogen, elixir-format
msgid "Hang in there while we get back on track"
msgstr ""
#: lib/exmr_web/live/exam_live/index.ex:34
#: lib/exmr_web/live/exam_live/index.html.heex:23
#, elixir-autogen, elixir-format
msgid "New Exam"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:57
#, elixir-autogen, elixir-format
msgid "Remove"
msgstr ""
#: lib/exmr_web/components/core_components.ex:171
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:17
#, elixir-autogen, elixir-format
msgid "Sort by Date"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:9
#, elixir-autogen, elixir-format
msgid "Sort by Subject"
msgstr ""
#: lib/exmr_web/components/core_components.ex:154
#, elixir-autogen, elixir-format
msgid "Success!"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:37
#, elixir-autogen, elixir-format
msgid "Today"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:39
#, elixir-autogen, elixir-format
msgid "Tomorrow"
msgid_plural "%{count} days left"
msgstr[0] ""
msgstr[1] ""
#: lib/exmr_web/components/core_components.ex:159
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
#: lib/exmr_web/components/core_components.ex:80
#: lib/exmr_web/components/core_components.ex:134
#, elixir-autogen, elixir-format
msgid "close"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:139
#, elixir-autogen, elixir-format
msgid "A link to confirm your email change has been sent to the new address."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:9
#, elixir-autogen, elixir-format
msgid "Account Settings"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:33
#, elixir-autogen, elixir-format
msgid "Change Email"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:76
#, elixir-autogen, elixir-format
msgid "Change Password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:32
#: lib/exmr_web/live/user_settings_live.ex:75
#, elixir-autogen, elixir-format
msgid "Changing..."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:63
#, elixir-autogen, elixir-format
msgid "Confirm new password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:27
#: lib/exmr_web/live/user_settings_live.ex:69
#, elixir-autogen, elixir-format
msgid "Current password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:21
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:92
#, elixir-autogen, elixir-format
msgid "Email change link is invalid or it has expired."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:89
#, elixir-autogen, elixir-format
msgid "Email changed successfully."
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:42
#, elixir-autogen, elixir-format
msgid "Log out"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:10
#, elixir-autogen, elixir-format
msgid "Manage your account email address and password settings"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:57
#, elixir-autogen, elixir-format
msgid "New password"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:33
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:26
#, elixir-autogen, elixir-format
msgid "Date"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:25
#, elixir-autogen, elixir-format
msgid "Description"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:24
#, elixir-autogen, elixir-format
msgid "Subject"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:13
#, elixir-autogen, elixir-format
msgid "Use this form to manage exam records in your database."
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:2
#, elixir-autogen, elixir-format
msgid "Listing Exams"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:28
#, elixir-autogen, elixir-format
msgid "Save Exam"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:28
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:40
#, elixir-autogen, elixir-format
msgid "Yesterday"
msgid_plural "%{count} days passed"
msgstr[0] ""
msgstr[1] ""
#: lib/exmr_web/live/exam_live/index.html.heex:38
#, elixir-autogen, elixir-format
msgid "Tommorow"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:59
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:51
#, elixir-autogen, elixir-format
msgid "Register"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:85
#, elixir-autogen, elixir-format
msgid "Fri"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Mon"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:85
#, elixir-autogen, elixir-format
msgid "Sat"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Sun"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Thu"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Tue"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Wed"
msgstr ""

View file

@ -0,0 +1,270 @@
## "msgid"s in this file come from POT (.pot) files.
###
### Do not add, change, or remove "msgid"s manually here as
### they're tied to the ones in the corresponding POT file
### (with the same domain).
###
### Use "mix gettext.extract --merge" or "mix gettext.merge"
### to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/exmr_web/controllers/page_html/home.html.heex:51
#, elixir-autogen, elixir-format
msgid "A simple, modern, and fast exam management system."
msgstr ""
#: lib/exmr_web/components/core_components.ex:489
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/exmr_web/components/core_components.ex:164
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:54
#, elixir-autogen, elixir-format
msgid "Built using Phoenix LiveView, Ecto, and TailwindCSS by @vavakado"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:50
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
#: lib/exmr_web/components/core_components.ex:155
#, elixir-autogen, elixir-format
msgid "Error!"
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:96
#, elixir-autogen, elixir-format
msgid "Exams"
msgstr ""
#: lib/exmr_web/components/core_components.ex:176
#, elixir-autogen, elixir-format
msgid "Hang in there while we get back on track"
msgstr ""
#: lib/exmr_web/live/exam_live/index.ex:34
#: lib/exmr_web/live/exam_live/index.html.heex:23
#, elixir-autogen, elixir-format
msgid "New Exam"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:57
#, elixir-autogen, elixir-format
msgid "Remove"
msgstr ""
#: lib/exmr_web/components/core_components.ex:171
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:17
#, elixir-autogen, elixir-format
msgid "Sort by Date"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:9
#, elixir-autogen, elixir-format
msgid "Sort by Subject"
msgstr ""
#: lib/exmr_web/components/core_components.ex:154
#, elixir-autogen, elixir-format
msgid "Success!"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:37
#, elixir-autogen, elixir-format
msgid "Today"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:39
#, elixir-autogen, elixir-format
msgid "Tomorrow"
msgid_plural "%{count} days left"
msgstr[0] ""
msgstr[1] ""
#: lib/exmr_web/components/core_components.ex:159
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
#: lib/exmr_web/components/core_components.ex:80
#: lib/exmr_web/components/core_components.ex:134
#, elixir-autogen, elixir-format
msgid "close"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:139
#, elixir-autogen, elixir-format
msgid "A link to confirm your email change has been sent to the new address."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:9
#, elixir-autogen, elixir-format
msgid "Account Settings"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:33
#, elixir-autogen, elixir-format
msgid "Change Email"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:76
#, elixir-autogen, elixir-format
msgid "Change Password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:32
#: lib/exmr_web/live/user_settings_live.ex:75
#, elixir-autogen, elixir-format
msgid "Changing..."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:63
#, elixir-autogen, elixir-format
msgid "Confirm new password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:27
#: lib/exmr_web/live/user_settings_live.ex:69
#, elixir-autogen, elixir-format
msgid "Current password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:21
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:92
#, elixir-autogen, elixir-format
msgid "Email change link is invalid or it has expired."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:89
#, elixir-autogen, elixir-format
msgid "Email changed successfully."
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:42
#, elixir-autogen, elixir-format
msgid "Log out"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:10
#, elixir-autogen, elixir-format
msgid "Manage your account email address and password settings"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:57
#, elixir-autogen, elixir-format
msgid "New password"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:33
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:26
#, elixir-autogen, elixir-format
msgid "Date"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:25
#, elixir-autogen, elixir-format
msgid "Description"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:24
#, elixir-autogen, elixir-format
msgid "Subject"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:13
#, elixir-autogen, elixir-format
msgid "Use this form to manage exam records in your database."
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:2
#, elixir-autogen, elixir-format
msgid "Listing Exams"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:28
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Exam"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:28
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:40
#, elixir-autogen, elixir-format
msgid "Yesterday"
msgid_plural "%{count} days passed"
msgstr[0] ""
msgstr[1] ""
#: lib/exmr_web/live/exam_live/index.html.heex:38
#, elixir-autogen, elixir-format, fuzzy
msgid "Tommorow"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:59
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:51
#, elixir-autogen, elixir-format
msgid "Register"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:85
#, elixir-autogen, elixir-format
msgid "Fri"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Mon"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:85
#, elixir-autogen, elixir-format
msgid "Sat"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Sun"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Thu"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Tue"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Wed"
msgstr ""

Binary file not shown.

View file

@ -0,0 +1,242 @@
# # This file is a PO Template file.
# #
# # "msgid"s here are often extracted from source code.
# # Add new messages manually only if they're dynamic
# # messages that can't be statically extracted.
# #
# # Run "mix gettext.extract" to bring this file up to
# # date. Leave "msgstr"s empty as changing them here has no
# # effect: edit them in PO (.po) files instead.
# Vladimir Rubin <vavakado@proton.me>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: unnamed project\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2024-12-23 19:01+0200\n"
"Last-Translator: Vladimir Rubin <vavakado@proton.me>\n"
"Language-Team: Russian\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Gtranslator 47.1\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: lib/exmr_web/controllers/page_html/home.html.heex:51
msgid "A simple, modern, and fast exam management system."
msgstr "Простая, современная и быстрая система управления экзаменами."
#: lib/exmr_web/components/core_components.ex:489
msgid "Actions"
msgstr "Действия"
#: lib/exmr_web/components/core_components.ex:164
msgid "Attempting to reconnect"
msgstr "Попытка восстановить соединение"
#: lib/exmr_web/controllers/page_html/home.html.heex:54
msgid "Built using Phoenix LiveView, Ecto, and TailwindCSS by @vavakado"
msgstr ""
"Создано с использованием Phoenix LiveView, Ecto и TailwindCSS в исполнении "
"@vavakado"
#: lib/exmr_web/live/exam_live/index.html.heex:50
msgid "Edit"
msgstr "Изменить"
#: lib/exmr_web/components/core_components.ex:155
msgid "Error!"
msgstr "Ошибка!"
#: lib/exmr_web/controllers/page_html/home.html.heex:96
msgid "Exams"
msgstr "Экзамены"
#: lib/exmr_web/components/core_components.ex:176
msgid "Hang in there while we get back on track"
msgstr "Держитесь, пока мы не вернемся в строй"
#: lib/exmr_web/live/exam_live/index.ex:34
#: lib/exmr_web/live/exam_live/index.html.heex:23
msgid "New Exam"
msgstr "Новый экзамен"
#: lib/exmr_web/live/exam_live/index.html.heex:57
msgid "Remove"
msgstr "Удалить"
#: lib/exmr_web/components/core_components.ex:171
msgid "Something went wrong!"
msgstr "Что-то пошло не так!"
#: lib/exmr_web/live/exam_live/index.html.heex:17
msgid "Sort by Date"
msgstr "Сортировать по дате"
#: lib/exmr_web/live/exam_live/index.html.heex:9
msgid "Sort by Subject"
msgstr "Сортировать по предмету"
#: lib/exmr_web/components/core_components.ex:154
msgid "Success!"
msgstr "Успех!"
#: lib/exmr_web/live/exam_live/index.html.heex:37
msgid "Today"
msgstr "Сегодня"
#: lib/exmr_web/live/exam_live/index.html.heex:39
msgid "Tomorrow"
msgid_plural "%{count} days left"
msgstr[0] "%{count} день остался"
msgstr[1] "%{count} дня осталось"
msgstr[2] "%{count} дней осталось"
#: lib/exmr_web/components/core_components.ex:159
msgid "We can't find the internet"
msgstr "Мы не можем найти интернет"
#: lib/exmr_web/components/core_components.ex:80
#: lib/exmr_web/components/core_components.ex:134
msgid "close"
msgstr "закрыть"
#: lib/exmr_web/live/user_settings_live.ex:139
msgid "A link to confirm your email change has been sent to the new address."
msgstr ""
"На новый адрес отправлена ссылка для подтверждения изменения электронной "
"почты."
#: lib/exmr_web/live/user_settings_live.ex:9
msgid "Account Settings"
msgstr "Настройки аккаунта"
#: lib/exmr_web/live/user_settings_live.ex:33
msgid "Change Email"
msgstr "Изменить электронную почту"
#: lib/exmr_web/live/user_settings_live.ex:76
msgid "Change Password"
msgstr "Изменить пароль"
#: lib/exmr_web/live/user_settings_live.ex:32
#: lib/exmr_web/live/user_settings_live.ex:75
msgid "Changing..."
msgstr "Меняется..."
#: lib/exmr_web/live/user_settings_live.ex:63
msgid "Confirm new password"
msgstr "Подтвердите новый пароль"
#: lib/exmr_web/live/user_settings_live.ex:27
#: lib/exmr_web/live/user_settings_live.ex:69
msgid "Current password"
msgstr "Текущий пароль"
#: lib/exmr_web/live/user_settings_live.ex:21
msgid "Email"
msgstr "Электронная почта"
#: lib/exmr_web/live/user_settings_live.ex:92
msgid "Email change link is invalid or it has expired."
msgstr ""
"Ссылка для изменения электронной почты недействительна или срок ее действия "
"истек."
#: lib/exmr_web/live/user_settings_live.ex:89
msgid "Email changed successfully."
msgstr "Электронная почта успешно изменена."
#: lib/exmr_web/components/layouts/root.html.heex:42
msgid "Log out"
msgstr "Выйти из системы"
#: lib/exmr_web/live/user_settings_live.ex:10
msgid "Manage your account email address and password settings"
msgstr "Управление адресом электронной почты и настройками пароля"
#: lib/exmr_web/live/user_settings_live.ex:57
msgid "New password"
msgstr "Новый пароль"
#: lib/exmr_web/components/layouts/root.html.heex:33
msgid "Settings"
msgstr "Настройки"
#: lib/exmr_web/live/exam_live/form_component.ex:26
msgid "Date"
msgstr "Дата"
#: lib/exmr_web/live/exam_live/form_component.ex:25
msgid "Description"
msgstr "Описание"
#: lib/exmr_web/live/exam_live/form_component.ex:24
msgid "Subject"
msgstr "Предмет"
#: lib/exmr_web/live/exam_live/form_component.ex:13
msgid "Use this form to manage exam records in your database."
msgstr ""
"Используйте эту форму для управления записями экзаменов в вашей базе данных."
#: lib/exmr_web/live/exam_live/index.html.heex:2
msgid "Listing Exams"
msgstr "Список Экзаменов"
#: lib/exmr_web/live/exam_live/form_component.ex:28
msgid "Save Exam"
msgstr "Сохранить экзамен"
#: lib/exmr_web/live/exam_live/form_component.ex:28
msgid "Saving..."
msgstr "Сохранение..."
#: lib/exmr_web/live/exam_live/index.html.heex:40
msgid "Yesterday"
msgid_plural "%{count} days passed"
msgstr[0] "%{count} день прошёл"
msgstr[1] "%{count} дня прошло"
msgstr[2] "%{count} дней прошло"
#: lib/exmr_web/live/exam_live/index.html.heex:38
msgid "Tommorow"
msgstr "Завтра"
#: lib/exmr_web/components/layouts/root.html.heex:59
msgid "Log in"
msgstr "Зайти в аккаунт"
#: lib/exmr_web/components/layouts/root.html.heex:51
msgid "Register"
msgstr "Зарегестрироваться"
#: lib/exmr_web/live/exam_live/index.html.heex:85
msgid "Fri"
msgstr "Пт"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Mon"
msgstr "Пн"
#: lib/exmr_web/live/exam_live/index.html.heex:85
msgid "Sat"
msgstr "Сб"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Sun"
msgstr "Вс"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Thu"
msgstr "Чт"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Tue"
msgstr "Вт"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Wed"
msgstr "Ср"

View file

@ -0,0 +1,122 @@
# # "msgid"s in this file come from POT (.pot) files.
# ##
# ## Do not add, change, or remove "msgid"s manually here as
# ## they're tied to the ones in the corresponding POT file
# ## (with the same domain).
# ##
# ## Use "mix gettext.extract --merge" or "mix gettext.merge"
# ## to merge POT files into PO files.
# Vladimir Rubin <vavakado@proton.me>, 2024.
#
msgid ""
msgstr ""
"Language: ru\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"MIME-Version: 1.0\n"
"Project-Id-Version: unnamed project\n"
"Last-Translator: Vladimir Rubin <vavakado@proton.me>\n"
"Language-Team: Russian\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2024-12-12 18:52+0200\n"
"X-Generator: Gtranslator 47.1\n"
msgid "can't be blank"
msgstr "не может быть пустым"
msgid "has already been taken"
msgstr "уже забрали"
msgid "is invalid"
msgstr "недействителен"
msgid "must be accepted"
msgstr "должен быть принят"
msgid "has invalid format"
msgstr "имеет неправильный формат"
msgid "has an invalid entry"
msgstr "имеет недопустимую запись"
msgid "is reserved"
msgstr "зарезервирован"
msgid "does not match confirmation"
msgstr "не соответствует подтверждению"
msgid "is still associated with this entry"
msgstr "по-прежнему связана с этой записью"
msgid "are still associated with this entry"
msgstr "по-прежнему связаны с этой записью"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] "должен иметь %{count} элементов"
msgstr[1] "должен иметь %{count} элемента"
msgstr[2] "должен иметь %{count} элементов"
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] "должен быть %{count} символов"
msgstr[1] "должен быть %{count} символа"
msgstr[2] "должен быть %{count} символов"
msgid "should be %{count} byte(s)"
msgid_plural "should be %{count} byte(s)"
msgstr[0] "должно быть %{count} байт"
msgstr[1] "должно быть %{count} байта"
msgstr[2] "должно быть %{count} байтов"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] "должно быть не менее %{count} элементов"
msgstr[1] "должно быть не менее %{count} элемента"
msgstr[2] "должно быть не менее %{count} элементов"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] "должно быть не менее %{count} символов"
msgstr[1] "должно быть не менее %{count} символа"
msgstr[2] "должно быть не менее %{count} символов"
msgid "should be at least %{count} byte(s)"
msgid_plural "should be at least %{count} byte(s)"
msgstr[0] "должно быть хотя бы %{count} байт"
msgstr[1] "должно быть хотя бы %{count} байта"
msgstr[2] "должно быть хотя бы %{count} байтов"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] "должно содержать не более %{count} элементов"
msgstr[1] "должно содержать не более %{count} элементов"
msgstr[2] "должно содержать не более %{count} элементов"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] "должно быть не более %{count} символа(ов)"
msgstr[1] "должно быть не более %{count} символа(ов)"
msgstr[2] "должно быть не более %{count} символа(ов)"
msgid "should be at most %{count} byte(s)"
msgid_plural "should be at most %{count} byte(s)"
msgstr[0] "должно быть не более %{count} байт(ов)."
msgstr[1] "должно быть не более %{count} байт(ов)."
msgstr[2] "должно быть не более %{count} байт(ов)."
msgid "must be less than %{number}"
msgstr "должно быть меньше, чем %{number}"
msgid "must be greater than %{number}"
msgstr "должно быть больше, чем %{number}"
msgid "must be less than or equal to %{number}"
msgstr "должно быть меньше или равно %{number}"
msgid "must be greater than or equal to %{number}"
msgstr "должно быть больше или равно %{number}"
msgid "must be equal to %{number}"
msgstr "должно быть равно %{number}"