Compare commits
5 Commits
42f8a88c61
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b47fcdffcb | |||
|
e955e40930
|
|||
| 11d923b72f | |||
|
dd507ca515
|
|||
| 9b2b4ad346 |
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
To start your Phoenix server:
|
To start your Phoenix server:
|
||||||
|
|
||||||
|
* Run `docker compose up -d` to run the development database
|
||||||
* Run `mix setup` to install and setup dependencies
|
* Run `mix setup` to install and setup dependencies
|
||||||
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
|
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"ignorePatterns": ["assets/vendor"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["typescript", "unicorn", "oxc"],
|
||||||
|
"ignorePatterns": ["assets/vendor"],
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error"
|
||||||
|
},
|
||||||
|
"rules": {},
|
||||||
|
"env": {
|
||||||
|
"builtin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.bg-boardwhite {
|
||||||
|
background-color: #f2f5f3; /* light square */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-boardblack {
|
||||||
|
background-color: #71828f; /* dark square */
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
.promotion-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-picker {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
background: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
background-size: 80% 80%;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece:hover {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece.queen.white {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik04IDEyYTIgMiAwIDEgMS00IDAgMiAyIDAgMSAxIDQgMHptMTYuNS00LjVhMiAyIDAgMSAxLTQgMCAyIDIgMCAxIDEgNCAwek00MSAxMmEyIDIgMCAxIDEtNCAwIDIgMiAwIDEgMSA0IDB6TTE2IDguNWEyIDIgMCAxIDEtNCAwIDIgMiAwIDEgMSA0IDB6TTMzIDlhMiAyIDAgMSAxLTQgMCAyIDIgMCAxIDEgNCAweiIvPjxwYXRoIGQ9Ik05IDI2YzguNS0xLjUgMjEtMS41IDI3IDBsMi0xMi03IDExVjExbC01LjUgMTMuNS0zLTE1LTMgMTUtNS41LTE0VjI1TDcgMTRsMiAxMnoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTkgMjZjMCAyIDEuNSAyIDIuNSA0IDEgMS41IDEgMSAuNSAzLjUtMS41IDEtMS41IDIuNS0xLjUgMi41LTEuNSAxLjUuNSAyLjUuNSAyLjUgNi41IDEgMTYuNSAxIDIzIDAgMCAwIDEuNS0xIDAtMi41IDAgMCAuNS0xLjUtMS0yLjUtLjUtMi41LS41LTIgLjUtMy41IDEtMiAyLjUtMiAyLjUtNC04LjUtMS41LTE4LjUtMS41LTI3IDB6IiBzdHJva2UtbGluZWNhcD0iYnV0dCIvPjxwYXRoIGQ9Ik0xMS41IDMwYzMuNS0xIDE4LjUtMSAyMiAwTTEyIDMzLjVjNi0xIDE1LTEgMjEgMCIgZmlsbD0ibm9uZSIvPjwvZz48L3N2Zz4=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece.rook.white {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik05IDM5aDI3di0zSDl2M3ptMy0zdi00aDIxdjRIMTJ6bS0xLTIyVjloNHYyaDVWOWg1djJoNVY5aDR2NSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz48cGF0aCBkPSJNMzQgMTRsLTMgM0gxNGwtMy0zIi8+PHBhdGggZD0iTTMxIDE3djEyLjVIMTRWMTciIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+PHBhdGggZD0iTTMxIDI5LjVsMS41IDIuNWgtMjBsMS41LTIuNSIvPjxwYXRoIGQ9Ik0xMSAxNGgyMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPjwvZz48L3N2Zz4=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece.bishop.white {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxnIGZpbGw9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJidXR0Ij48cGF0aCBkPSJNOSAzNmMzLjM5LS45NyAxMC4xMS40MyAxMy41LTIgMy4zOSAyLjQzIDEwLjExIDEuMDMgMTMuNSAyIDAgMCAxLjY1LjU0IDMgMi0uNjguOTctMS42NS45OS0zIC41LTMuMzktLjk3LTEwLjExLjQ2LTEzLjUtMS0zLjM5IDEuNDYtMTAuMTEuMDMtMTMuNSAxLTEuMzU0LjQ5LTIuMzIzLjQ3LTMtLjUgMS4zNTQtMS45NCAzLTIgMy0yeiIvPjxwYXRoIGQ9Ik0xNSAzMmMyLjUgMi41IDEyLjUgMi41IDE1IDAgLjUtMS41IDAtMiAwLTIgMC0yLjUtMi41LTQtMi41LTQgNS41LTEuNSA2LTExLjUtNS0xNS41LTExIDQtMTAuNSAxNC01IDE1LjUgMCAwLTIuNSAxLjUtMi41IDQgMCAwLS41LjUgMCAyeiIvPjxwYXRoIGQ9Ik0yNSA4YTIuNSAyLjUgMCAxIDEtNSAwIDIuNSAyLjUgMCAxIDEgNSAweiIvPjwvZz48cGF0aCBkPSJNMTcuNSAyNmgxME0xNSAzMGgxNW0tNy41LTE0LjV2NU0yMCAxOGg1IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+PC9nPjwvc3ZnPg==");
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece.knight.white {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMiAxMGMxMC41IDEgMTYuNSA4IDE2IDI5SDE1YzAtOSAxMC02LjUgOC0yMSIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0yNCAxOGMuMzggMi45MS01LjU1IDcuMzctOCA5LTMgMi0yLjgyIDQuMzQtNSA0LTEuMDQyLS45NCAxLjQxLTMuMDQgMC0zLTEgMCAuMTkgMS4yMy0xIDItMSAwLTQuMDAzIDEtNC00IDAtMiA2LTEyIDYtMTJzMS44OS0xLjkgMi0zLjVjLS43My0uOTk0LS41LTItLjUtMyAxLTEgMyAyLjUgMyAyLjVoMnMuNzgtMS45OTIgMi41LTNjMSAwIDEgMyAxIDMiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNOS41IDI1LjVhLjUuNSAwIDEgMS0xIDAgLjUuNSAwIDEgMSAxIDB6bTUuNDMzLTkuNzVhLjUgMS41IDMwIDEgMS0uODY2LS41LjUgMS41IDMwIDEgMSAuODY2LjV6IiBmaWxsPSIjMDAwIi8+PC9nPjwvc3ZnPg==");
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece.queen.black {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxnIHN0cm9rZT0ibm9uZSI+PGNpcmNsZSBjeD0iNiIgY3k9IjEyIiByPSIyLjc1Ii8+PGNpcmNsZSBjeD0iMTQiIGN5PSI5IiByPSIyLjc1Ii8+PGNpcmNsZSBjeD0iMjIuNSIgY3k9IjgiIHI9IjIuNzUiLz48Y2lyY2xlIGN4PSIzMSIgY3k9IjkiIHI9IjIuNzUiLz48Y2lyY2xlIGN4PSIzOSIgY3k9IjEyIiByPSIyLjc1Ii8+PC9nPjxwYXRoIGQ9Ik05IDI2YzguNS0xLjUgMjEtMS41IDI3IDBsMi41LTEyLjVMMzEgMjVsLS4zLTE0LjEtNS4yIDEzLjYtMy0xNC41LTMgMTQuNS01LjItMTMuNkwxNCAyNSA2LjUgMTMuNSA5IDI2eiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz48cGF0aCBkPSJNOSAyNmMwIDIgMS41IDIgMi41IDQgMSAxLjUgMSAxIC41IDMuNS0xLjUgMS0xLjUgMi41LTEuNSAyLjUtMS41IDEuNS41IDIuNS41IDIuNSA2LjUgMSAxNi41IDEgMjMgMCAwIDAgMS41LTEgMC0yLjUgMCAwIC41LTEuNS0xLTIuNS0uNS0yLjUtLjUtMiAuNS0zLjUgMS0yIDIuNS0yIDIuNS00LTguNS0xLjUtMTguNS0xLjUtMjcgMHoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTExIDM4LjVhMzUgMzUgMSAwIDAgMjMgMCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz48cGF0aCBkPSJNMTEgMjlhMzUgMzUgMSAwIDEgMjMgMG0tMjEuNSAyLjVoMjBtLTIxIDNhMzUgMzUgMSAwIDAgMjIgMG0tMjMgM2EzNSAzNSAxIDAgMCAyNCAwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlY2VjZWMiLz48L2c+PC9zdmc+");
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece.rook.black {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik05IDM5aDI3di0zSDl2M3ptMy41LTdsMS41LTIuNWgxN2wxLjUgMi41aC0yMHptLS41IDR2LTRoMjF2NEgxMnoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTE0IDI5LjV2LTEzaDE3djEzSDE0eiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMTQgMTYuNUwxMSAxNGgyM2wtMyAyLjVIMTR6TTExIDE0VjloNHYyaDVWOWg1djJoNVY5aDR2NUgxMXoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTEyIDM1LjVoMjFtLTIwLTRoMTltLTE4LTJoMTdtLTE3LTEzaDE3TTExIDE0aDIzIiBmaWxsPSJub25lIiBzdHJva2U9IiNlY2VjZWMiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPjwvZz48L3N2Zz4=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece.bishop.black {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxnIGZpbGw9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJidXR0Ij48cGF0aCBkPSJNOSAzNmMzLjM5LS45NyAxMC4xMS40MyAxMy41LTIgMy4zOSAyLjQzIDEwLjExIDEuMDMgMTMuNSAyIDAgMCAxLjY1LjU0IDMgMi0uNjguOTctMS42NS45OS0zIC41LTMuMzktLjk3LTEwLjExLjQ2LTEzLjUtMS0zLjM5IDEuNDYtMTAuMTEuMDMtMTMuNSAxLTEuMzU0LjQ5LTIuMzIzLjQ3LTMtLjUgMS4zNTQtMS45NCAzLTIgMy0yeiIvPjxwYXRoIGQ9Ik0xNSAzMmMyLjUgMi41IDEyLjUgMi41IDE1IDAgLjUtMS41IDAtMiAwLTIgMC0yLjUtMi41LTQtMi41LTQgNS41LTEuNSA2LTExLjUtNS0xNS41LTExIDQtMTAuNSAxNC01IDE1LjUgMCAwLTIuNSAxLjUtMi41IDQgMCAwLS41LjUgMCAyeiIvPjxwYXRoIGQ9Ik0yNSA4YTIuNSAyLjUgMCAxIDEtNSAwIDIuNSAyLjUgMCAxIDEgNSAweiIvPjwvZz48cGF0aCBkPSJNMTcuNSAyNmgxME0xNSAzMGgxNW0tNy41LTE0LjV2NU0yMCAxOGg1IiBzdHJva2U9IiNlY2VjZWMiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48L2c+PC9zdmc+");
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-piece.knight.black {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMiAxMGMxMC41IDEgMTYuNSA4IDE2IDI5SDE1YzAtOSAxMC02LjUgOC0yMSIgZmlsbD0iIzAwMCIvPjxwYXRoIGQ9Ik0yNCAxOGMuMzggMi45MS01LjU1IDcuMzctOCA5LTMgMi0yLjgyIDQuMzQtNSA0LTEuMDQyLS45NCAxLjQxLTMuMDQgMC0zLTEgMCAuMTkgMS4yMy0xIDItMSAwLTQuMDAzIDEtNC00IDAtMiA2LTEyIDYtMTJzMS44OS0xLjkgMi0zLjVjLS43My0uOTk0LS41LTItLjUtMyAxLTEgMyAyLjUgMyAyLjVoMnMuNzgtMS45OTIgMi41LTNjMSAwIDEgMyAxIDMiIGZpbGw9IiMwMDAiLz48cGF0aCBkPSJNOS41IDI1LjVhLjUuNSAwIDEgMS0xIDAgLjUuNSAwIDEgMSAxIDB6bTUuNDMzLTkuNzVhLjUgMS41IDMwIDEgMS0uODY2LS41LjUgMS41IDMwIDEgMSAuODY2LjV6IiBmaWxsPSIjZWNlY2VjIiBzdHJva2U9IiNlY2VjZWMiLz48cGF0aCBkPSJNMjQuNTUgMTAuNGwtLjQ1IDEuNDUuNS4xNWMzLjE1IDEgNS42NSAyLjQ5IDcuOSA2Ljc1UzM1Ljc1IDI5LjA2IDM1LjI1IDM5bC0uMDUuNWgyLjI1bC4wNS0uNWMuNS0xMC4wNi0uODgtMTYuODUtMy4yNS0yMS4zNC0yLjM3LTQuNDktNS43OS02LjY0LTkuMTktNy4xNmwtLjUxLS4xeiIgZmlsbD0iI2VjZWNlYyIgc3Ryb2tlPSJub25lIi8+PC9nPjwvc3ZnPg==");
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
@import "../node_modules/chessground/assets/chessground.brown.css";
|
@import "../node_modules/chessground/assets/chessground.brown.css";
|
||||||
@import "../node_modules/chessground/assets/chessground.cburnett.css";
|
@import "../node_modules/chessground/assets/chessground.cburnett.css";
|
||||||
|
|
||||||
|
@import "./_board.css";
|
||||||
|
@import "./_promotion.css";
|
||||||
|
|
||||||
@source "../css";
|
@source "../css";
|
||||||
@source "../js";
|
@source "../js";
|
||||||
@source "../../lib/chesstrainer_web";
|
@source "../../lib/chesstrainer_web";
|
||||||
@@ -108,6 +111,9 @@
|
|||||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||||
|
|
||||||
/* Make LiveView wrapper divs transparent for layout */
|
/* Make LiveView wrapper divs transparent for layout */
|
||||||
[data-phx-session], [data-phx-teleported-src] { display: contents }
|
[data-phx-session],
|
||||||
|
[data-phx-teleported-src] {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
/* This file is for your main application CSS */
|
/* This file is for your main application CSS */
|
||||||
|
|||||||
@@ -18,39 +18,39 @@
|
|||||||
// To load it, simply add a second `<link>` to your `root.html.heex` file.
|
// To load it, simply add a second `<link>` to your `root.html.heex` file.
|
||||||
|
|
||||||
// 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 {hooks as colocatedHooks} from "phoenix-colocated/chesstrainer"
|
import { hooks as colocatedHooks } from "phoenix-colocated/chesstrainer";
|
||||||
import topbar from "../vendor/topbar"
|
import topbar from "../vendor/topbar";
|
||||||
import { ChessBoard } from "./hooks/chessBoard"
|
import { ChessBoard } from "./hooks/chessBoard";
|
||||||
|
|
||||||
let Hooks = {
|
let Hooks = {
|
||||||
...colocatedHooks,
|
...colocatedHooks,
|
||||||
ChessBoard
|
ChessBoard,
|
||||||
}
|
};
|
||||||
|
|
||||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
|
||||||
const liveSocket = new LiveSocket("/live", Socket, {
|
const liveSocket = new LiveSocket("/live", Socket, {
|
||||||
longPollFallbackMs: 2500,
|
longPollFallbackMs: 2500,
|
||||||
params: {_csrf_token: csrfToken},
|
params: { _csrf_token: csrfToken },
|
||||||
hooks: Hooks,
|
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;
|
||||||
|
|
||||||
// The lines below enable quality of life phoenix_live_reload
|
// The lines below enable quality of life phoenix_live_reload
|
||||||
// development features:
|
// development features:
|
||||||
@@ -59,31 +59,34 @@ window.liveSocket = liveSocket
|
|||||||
// 2. click on elements to jump to their definitions in your code editor
|
// 2. click on elements to jump to their definitions in your code editor
|
||||||
//
|
//
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
|
window.addEventListener("phx:live_reload:attached", ({ detail: reloader }) => {
|
||||||
// Enable server log streaming to client.
|
// Enable server log streaming to client.
|
||||||
// Disable with reloader.disableServerLogs()
|
// Disable with reloader.disableServerLogs()
|
||||||
reloader.enableServerLogs()
|
reloader.enableServerLogs();
|
||||||
|
|
||||||
// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
|
// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
|
||||||
//
|
//
|
||||||
// * click with "c" key pressed to open at caller location
|
// * click with "c" key pressed to open at caller location
|
||||||
// * click with "d" key pressed to open at function component definition location
|
// * click with "d" key pressed to open at function component definition location
|
||||||
let keyDown
|
let keyDown;
|
||||||
window.addEventListener("keydown", e => keyDown = e.key)
|
window.addEventListener("keydown", (e) => (keyDown = e.key));
|
||||||
window.addEventListener("keyup", _e => keyDown = null)
|
window.addEventListener("keyup", (_e) => (keyDown = null));
|
||||||
window.addEventListener("click", e => {
|
window.addEventListener(
|
||||||
if(keyDown === "c"){
|
"click",
|
||||||
e.preventDefault()
|
(e) => {
|
||||||
e.stopImmediatePropagation()
|
if (keyDown === "c") {
|
||||||
reloader.openEditorAtCaller(e.target)
|
e.preventDefault();
|
||||||
} else if(keyDown === "d"){
|
e.stopImmediatePropagation();
|
||||||
e.preventDefault()
|
reloader.openEditorAtCaller(e.target);
|
||||||
e.stopImmediatePropagation()
|
} else if (keyDown === "d") {
|
||||||
reloader.openEditorAtDef(e.target)
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
reloader.openEditorAtDef(e.target);
|
||||||
}
|
}
|
||||||
}, true)
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
window.liveReloader = reloader
|
window.liveReloader = reloader;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +1,122 @@
|
|||||||
import { Chessground } from 'chessground';
|
import { Chessground } from "chessground";
|
||||||
import { Chess } from 'chess.js';
|
import { Chess } from "chess.js";
|
||||||
|
|
||||||
export const ChessBoard = {
|
export const ChessBoard = {
|
||||||
mounted() {
|
mounted() {
|
||||||
// Initialize chess.js for local rule validation
|
|
||||||
this.chess = new Chess();
|
this.chess = new Chess();
|
||||||
|
|
||||||
// Initialize Chessground
|
|
||||||
this.ground = Chessground(this.el, {
|
this.ground = Chessground(this.el, {
|
||||||
fen: this.chess.fen(),
|
fen: this.chess.fen(),
|
||||||
movable: {
|
movable: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
free: false, // Don't allow completely arbitrary drops
|
free: false,
|
||||||
dests: this.getValidDests() // Only allow legal chess moves
|
dests: this.getValidDests(),
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
// Triggered when a piece is successfully dropped
|
move: (orig, dest, _capturedPiece) => {
|
||||||
move: (orig, dest, _metadata) => {
|
|
||||||
this.handleMove(orig, dest);
|
this.handleMove(orig, dest);
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handleEvent("set_fen", ({ fen }) => {
|
||||||
|
try {
|
||||||
|
this.chess.load(fen);
|
||||||
|
|
||||||
|
const currentTurnColor = this.chess.turn() === "w" ? "white" : "black";
|
||||||
|
|
||||||
|
this.ground.set({
|
||||||
|
fen: fen,
|
||||||
|
turnColor: currentTurnColor,
|
||||||
|
movable: {
|
||||||
|
color: currentTurnColor,
|
||||||
|
dests: this.getValidDests(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Invalid FEN string submitted:", error);
|
||||||
|
alert("Invalid FEN configuration!");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getValidDests() {
|
getValidDests() {
|
||||||
// Convert chess.js moves into the Map format Chessground expects
|
|
||||||
const dests = new Map();
|
const dests = new Map();
|
||||||
this.chess.moves({ verbose: true }).forEach(m => {
|
|
||||||
|
this.chess.moves({ verbose: true }).forEach((m) => {
|
||||||
if (!dests.has(m.from)) dests.set(m.from, []);
|
if (!dests.has(m.from)) dests.set(m.from, []);
|
||||||
dests.get(m.from).push(m.to);
|
dests.get(m.from).push(m.to);
|
||||||
});
|
});
|
||||||
|
|
||||||
return dests;
|
return dests;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMove(orig, dest) {
|
handleMove(orig, dest) {
|
||||||
// 1. Try to execute the move in chess.js state
|
const piece = this.ground.state.pieces.get(dest);
|
||||||
const move = this.chess.move({ from: orig, to: dest, promotion: 'q' });
|
const destRank = dest[1];
|
||||||
|
if (piece?.role === "pawn" && (destRank === "1" || destRank === "8")) {
|
||||||
|
this.showPromotionDialog(orig, dest);
|
||||||
|
return; // don't execute yet — wait for user choice
|
||||||
|
}
|
||||||
|
this.executeMove(orig, dest);
|
||||||
|
},
|
||||||
|
|
||||||
|
showPromotionDialog(orig, dest) {
|
||||||
|
this.pendingPromotion = { orig, dest };
|
||||||
|
const color = this.chess.turn() === "w" ? "white" : "black";
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.className = "promotion-overlay";
|
||||||
|
const picker = document.createElement("div");
|
||||||
|
picker.className = "promotion-picker";
|
||||||
|
const pieces = [
|
||||||
|
{ type: "queen", code: "q" },
|
||||||
|
{ type: "rook", code: "r" },
|
||||||
|
{ type: "bishop", code: "b" },
|
||||||
|
{ type: "knight", code: "n" },
|
||||||
|
];
|
||||||
|
pieces.forEach(({ type, code }) => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.className = `promotion-piece ${type} ${color}`;
|
||||||
|
btn.addEventListener("click", () => this.completePromotion(code));
|
||||||
|
picker.appendChild(btn);
|
||||||
|
});
|
||||||
|
overlay.appendChild(picker);
|
||||||
|
const boardContainer = this.el.closest(".shadow-inner");
|
||||||
|
boardContainer.style.position = "relative";
|
||||||
|
boardContainer.appendChild(overlay);
|
||||||
|
},
|
||||||
|
|
||||||
|
completePromotion(pieceType) {
|
||||||
|
const { orig, dest } = this.pendingPromotion;
|
||||||
|
this.pendingPromotion = null;
|
||||||
|
const picker = this.el.closest(".shadow-inner")?.querySelector(".promotion-overlay");
|
||||||
|
if (picker) picker.remove();
|
||||||
|
this.executeMove(orig, dest, pieceType);
|
||||||
|
},
|
||||||
|
|
||||||
|
executeMove(orig, dest, promotion) {
|
||||||
|
const move = this.chess.move({ from: orig, to: dest, promotion });
|
||||||
|
|
||||||
if (move) {
|
if (move) {
|
||||||
// 2. Telemetry back up to Phoenix LiveView
|
|
||||||
this.pushEvent("move_played", {
|
this.pushEvent("move_played", {
|
||||||
from: orig,
|
from: orig,
|
||||||
to: dest,
|
to: dest,
|
||||||
fen: this.chess.fen()
|
fen: this.chess.fen(),
|
||||||
|
san: move.san,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Determine whose turn it is now ('w' or 'b')
|
const nextTurnColor = this.chess.turn() === "w" ? "white" : "black";
|
||||||
const nextTurnColor = this.chess.turn() === 'w' ? 'white' : 'black';
|
|
||||||
|
|
||||||
// 4. Force Chessground to update its internal state for the next player
|
|
||||||
this.ground.set({
|
this.ground.set({
|
||||||
turnColor: nextTurnColor, // Switches turn tracking
|
turnColor: nextTurnColor,
|
||||||
|
fen: this.chess.fen(),
|
||||||
movable: {
|
movable: {
|
||||||
color: nextTurnColor, // 👈 THIS IS CRITICAL: Changes who is allowed to drag pieces!
|
color: nextTurnColor,
|
||||||
dests: this.getValidDests() // Re-calculates valid legal squares for Black
|
dests: this.getValidDests(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If chess.js says it's an illegal move, snap the piece back instantly
|
|
||||||
this.ground.set({ fen: this.chess.fen() });
|
this.ground.set({ fen: this.chess.fen() });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -11,6 +11,704 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chess.js": "^1.4.0",
|
"chess.js": "^1.4.0",
|
||||||
"chessground": "^9.2.1"
|
"chessground": "^9.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"oxfmt": "^0.55.0",
|
||||||
|
"oxlint": "^1.70.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-android-arm-eabi": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-+rFDOqQe5LOWgxrAJaZgLRudr6GQm0wGI6gtu7vVkrdLGjNMUSGbAlaCr8j7F2H2Er97vYQCU8WDb30onqMM1g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-android-arm64": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-ctulLq8s3x8Zmvw6+iccB09TIKERAklRSmbJ10gk8mlAn05qZxoyo52dj3Hi9IJcmDSwF54fQaTVh2CbL6PInw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-darwin-arm64": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-xDQczLH9pw/RBk1h/GH0qcGMm8hQtmtVHBNLSH3lk1gEIR09hZ4L+mJQl4VqiVAvPK9VG9PYrWWuSQLt7xTbiA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-darwin-x64": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-JaNoFCkF2CJdGgpPSMbuO9HVyXyoNGIhMHPvp6NYAjeVKw9XEYc0HcUWJLPQa3Q69WV5wMa9m5jPMJPtbLtcRg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-freebsd-x64": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-DNbszhpg6S2MIzax5azdHFTTBIVkR5xr8yyRZuA4yoDAwOkzIp3tmldgKZM2+VlT+hJIG0xUksA+elISzMEAfA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-arm-gnueabihf": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-2snoaoRfFFyGnbOcKUK36rREBYxe/Xgz3uHbiA5zbCB/s6R4DQj4mHqYAaWWhgizCUSDxV8cE9zAZ0XleNpKGw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-arm-musleabihf": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-q1aktHF/WRpSK81BX1dE/9vWrS2jGw1Nax2kb4DBLGAewubCLcoNyp4Zl/NSMgbv3vUS46Z33wIQkBVYOP3PYg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-arm64-gnu": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-VD0y36aENezl/3tsclA/4G53Cc7iV+7Uoh7gz4yvcOTaEYBtJpQsE6PKDGTtUtOvGS4kv51ybfXY/nWZejO5IA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-arm64-musl": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-r8xlKJFcsRmn0H5jZrdORae6RX9jDBrZVvOoxF+bCQtampQJClv80aZEHsv+NsLsp2KCE5ql79O7DpPVzYWpXA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-ppc64-gnu": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-GRKv/HXHcwIVld/WU61rF0g0R16hl5EJ+ScKdpjevT57lnLnagj/U2YUbXf2mT+2Pg1uCzWC+mvGicPV3CDdLQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-riscv64-gnu": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-rdv57enTiPtpSYRMKfAiEbQb0Puw5t9N7isVinDoo5qeLDScro2gznmZqSgSWbVZRzLisTeCTW8Qwgw0bOHv3A==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-riscv64-musl": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-7v1nNrlD43VY6+sYQ6efYyb3lE6QY182304PD/768ZxTjOmFd/3dQa3u/nGBUAXYdGSWOQc5N3PnS0QzUXyEIA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-s390x-gnu": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-f4lJLUSPOgScjFl9LiflKCTocyNRwE25JmTMbN4XQdDjoZzEHjqf3wA3VESF1/csg7i8m7+EQLbrZyYDqe10UQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-x64-gnu": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-MihqiPziJNoWy4MqNSV+jVA1g+07iQDjZiR0vaCaDoPgFEiJpCMsxamktzLV07cEeQsSJ04vQaU4CzCQwIvtDA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-linux-x64-musl": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-Yqghym7KYAVjP9MmSrNZiDeerMuoejNjo0r3ox5H3GDKk8eAfl8VyJm9i+pWCLDCTnAbcTUMMN2ZKjUYXH1v3g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-openharmony-arm64": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-s5SDvVVSbyQl1V5UU3Yl12M+XLUQ3rl5SglNqgAA2K4PXUtQhyNSS00wivONPEnNo5W01rCou8WkDNyvI/RGHg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-win32-arm64-msvc": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-7p9FB5R32tw2KyyNX3wpQrR2WHwEHvMEiBlGXxeTCaRMCVNx3UtFMAUbaQ/pRNWIrEUZmYhJ6tcUH52uPTRYjQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-win32-ia32-msvc": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-ZYqj3fDnOT1IaVGMP5kpmkQl4F3tQIm2ZyAxvqkJYmI0xgWWak4ss4XYwv3VDfM+TWXeC9K4uQ/wW5jm/5XABA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxfmt/binding-win32-x64-msvc": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-eEYT5tivGnGbPHuOHuQpi6CGLObhh0re/5jcNQHihD2GRYkTM85dyi5a19zjP8Q00t1uqAx+/QGLUGdHeqzWyg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-android-arm-eabi": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-zFh0P4cswmRvw6nkyb89dr18rRanuaCPAsEXsFDoQY8WdaquI8Pt4NWFjaMJg6L23cy5NeN8J9cBnREbWzZhaw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-android-arm64": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-qI8o4HZjeGiBrWv+pJv4lH0Yi2Gl/JSp/EumBUApezJprIKa5PS4nU0lQsQngtky8k+SplQIOjv6hwu0SSxeyg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-darwin-arm64": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-8KjgVVHI5F9nVwHCRwwA78Ty7zNKP4Wd9OeN5PSv3iu/F/u1RVXoOCgLhWqust6HmwQG6xc8c+RCyaWENy24+w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-darwin-x64": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-WVydssv5PSUBXFJTdNBWlmGkbNmvPGaFt/2SUT/EZRB6bq6bEOHmMlbnupZD5jmlEvi9+mZJHi8TCw15lyfSfQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-freebsd-x64": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-hJucmUf8OlinHNb1R7fI4Fw6WsAstOz7i8nmkWQfiHoZXtbufNm+MxiDTIMk1ggh2Ro4vLzgQ+bKvRY54MZoRA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-1BnS7wbCYDSXwWzJJ+mc3NURoha6m6m6RT5c6vgAY3oz7C3OVXP+S0awo2mRq97arrJkVvO3qRQfyAHL+76xtQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-yKy/UdbR55+M2yEcuiV5DCNC/gdQAjr/GioUy50QwBzSrKm8ueWADqyRLS9Xk+qjNeCYGg6A8FvUBds56ttfqg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-arm64-gnu": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-0A5XJ4alvmqFUFP/4oYSyaO+qLto/HrKEWTSaegiVl+HOufFngK2BjYw9x4RbwBt/du5QG6l5q1zeWiJYYG5yg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-arm64-musl": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-JiylyurlB0CLSedNtx1gzv3FvfWPF1h/2Y3BJszPLNt5XQFlBsH5ke0Jle3iJb3uqu5m2e7A/DwzpuCAHdiU+A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-J8VPG7I3/HmgaU4u8pNU2kFx2+0U+vPLS1dXFxXOaR/2TQ0f8AC7DRz0SRGRI1bfphnX2hVYTTtLuhL4nYKL+Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-N2+4lV2KLN+oXTIIIwmWDhwkrnvqf5oX7Hw0zPjk+RuIVgiBQSOlJWF7uQoFx2siEYX0ZQ5cfSbEAHm+J3t7Wg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-riscv64-musl": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-1e2L7cFCvx9QDzq6NPP+0tABKb5z6nWHyddWTNKprEsjO9xNrAtPowuCGpjNXxkTdsMiZ4jc8YQ5SstZd4XK6g==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-s390x-gnu": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-Kwu/l/8GcYibCWA9m9N5pRXMIKVSsL/YbgpLzYkqDhWTiqdRfnNJ/+nqIKRKQiFbHWsdlHEhzMwruJK+qcEruA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-x64-gnu": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-tap04CsHYOl0nSAQJfPNIuBxqEPB2HnhQqwaOXLg1jnp2XfRo8Fa814dA4QC4zpvTWXCjAAaCY1W5LOORkEQuQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-linux-x64-musl": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-hzJa/WgvtJpbBD9rgfy0qe+MjbxOXNUT0bfR1S6EQQzfTtBFA9xg5q8KSwRrQ2QfSS+TaP4j+4mVPQrfNc6UNg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-openharmony-arm64": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-xbsaNSNzVSnaJACCUYr1HQMyY/Q/Q1LkePmHG3UvZPvGCYGNxrsZp9OmtA6ick8xH47ltRRbRrPCM1YXYcyC+A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-win32-arm64-msvc": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-icAEsUI7JbW1TMRdEXV83mVAInhRVQYuuAlPpxdGwJ95chNdnCzjloRW8GglT0WvzOEZSio6fnYSk2DJ2Hv7LQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-win32-ia32-msvc": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-FHMSWbVsPVs/f+Jcl04ws4JJ2wUnauyTzlpxWRG/lSO/8GpX08Fo2gQZqdA6CrRFI+zvkxl+N/KwJGWfUwYVZA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxlint/binding-win32-x64-msvc": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-ptOlKwCz7n4AKs5VweMqG6DAg677FmKOK+vBkkL9DMNgFATIQ+upqUYBTOEwRQyRAx1ncGlPlXleV2hIcm3z4g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chess.js": {
|
"node_modules/chess.js": {
|
||||||
@@ -28,6 +726,117 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://lichess.org/patron"
|
"url": "https://lichess.org/patron"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/oxfmt": {
|
||||||
|
"version": "0.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.55.0.tgz",
|
||||||
|
"integrity": "sha512-jSj2wCTakwgPMxkfiVZX0jf+nX+Nz6xlyAZjqNE0qXTFdCBPYlP6JAN+ODjmealw7DXBjOzYbdsqwBMAZnPZ6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinypool": "2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"oxfmt": "bin/oxfmt"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@oxfmt/binding-android-arm-eabi": "0.55.0",
|
||||||
|
"@oxfmt/binding-android-arm64": "0.55.0",
|
||||||
|
"@oxfmt/binding-darwin-arm64": "0.55.0",
|
||||||
|
"@oxfmt/binding-darwin-x64": "0.55.0",
|
||||||
|
"@oxfmt/binding-freebsd-x64": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-arm-gnueabihf": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-arm-musleabihf": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-arm64-gnu": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-arm64-musl": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-ppc64-gnu": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-riscv64-gnu": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-riscv64-musl": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-s390x-gnu": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-x64-gnu": "0.55.0",
|
||||||
|
"@oxfmt/binding-linux-x64-musl": "0.55.0",
|
||||||
|
"@oxfmt/binding-openharmony-arm64": "0.55.0",
|
||||||
|
"@oxfmt/binding-win32-arm64-msvc": "0.55.0",
|
||||||
|
"@oxfmt/binding-win32-ia32-msvc": "0.55.0",
|
||||||
|
"@oxfmt/binding-win32-x64-msvc": "0.55.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"vite-plus": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"svelte": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite-plus": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/oxlint": {
|
||||||
|
"version": "1.70.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.70.0.tgz",
|
||||||
|
"integrity": "sha512-D6JgHtzkhRwvEC+A0Nw5AEc5bk8x5i1pHzvZIEf/a0C4hOzmAACNGtkDGPyFaxxX3ZVGxCPeig3P3rMM8XU3/g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"oxlint": "bin/oxlint"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@oxlint/binding-android-arm-eabi": "1.70.0",
|
||||||
|
"@oxlint/binding-android-arm64": "1.70.0",
|
||||||
|
"@oxlint/binding-darwin-arm64": "1.70.0",
|
||||||
|
"@oxlint/binding-darwin-x64": "1.70.0",
|
||||||
|
"@oxlint/binding-freebsd-x64": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-arm-gnueabihf": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-arm-musleabihf": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-arm64-gnu": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-arm64-musl": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-ppc64-gnu": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-riscv64-gnu": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-riscv64-musl": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-s390x-gnu": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-x64-gnu": "1.70.0",
|
||||||
|
"@oxlint/binding-linux-x64-musl": "1.70.0",
|
||||||
|
"@oxlint/binding-openharmony-arm64": "1.70.0",
|
||||||
|
"@oxlint/binding-win32-arm64-msvc": "1.70.0",
|
||||||
|
"@oxlint/binding-win32-ia32-msvc": "1.70.0",
|
||||||
|
"@oxlint/binding-win32-x64-msvc": "1.70.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"oxlint-tsgolint": ">=0.22.1",
|
||||||
|
"vite-plus": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"oxlint-tsgolint": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite-plus": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinypool": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.0.0 || >=22.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,18 @@
|
|||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"fmt": "oxfmt --check",
|
||||||
|
"fmt:fix": "oxfmt",
|
||||||
|
"lint": "oxlint",
|
||||||
|
"lint:fix": "oxlint --fix",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chess.js": "^1.4.0",
|
"chess.js": "^1.4.0",
|
||||||
"chessground": "^9.2.1"
|
"chessground": "^9.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"oxfmt": "^0.55.0",
|
||||||
|
"oxlint": "^1.70.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
// Feel free to adjust this configuration however you need.
|
// Feel free to adjust this configuration however you need.
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"*": ["../deps/*"]
|
"*": ["../deps/*"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,43 +1,49 @@
|
|||||||
const plugin = require("tailwindcss/plugin")
|
const plugin = require("tailwindcss/plugin");
|
||||||
const fs = require("fs")
|
const fs = require("fs");
|
||||||
const path = require("path")
|
const path = require("path");
|
||||||
|
|
||||||
module.exports = plugin(function({matchComponents, theme}) {
|
module.exports = plugin(function ({ matchComponents, theme }) {
|
||||||
let iconsDir = path.join(__dirname, "../../deps/heroicons/optimized")
|
let iconsDir = path.join(__dirname, "../../deps/heroicons/optimized");
|
||||||
let values = {}
|
let values = {};
|
||||||
let icons = [
|
let icons = [
|
||||||
["", "/24/outline"],
|
["", "/24/outline"],
|
||||||
["-solid", "/24/solid"],
|
["-solid", "/24/solid"],
|
||||||
["-mini", "/20/solid"],
|
["-mini", "/20/solid"],
|
||||||
["-micro", "/16/solid"]
|
["-micro", "/16/solid"],
|
||||||
]
|
];
|
||||||
icons.forEach(([suffix, dir]) => {
|
icons.forEach(([suffix, dir]) => {
|
||||||
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
|
fs.readdirSync(path.join(iconsDir, dir)).forEach((file) => {
|
||||||
let name = path.basename(file, ".svg") + suffix
|
let name = path.basename(file, ".svg") + suffix;
|
||||||
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
|
values[name] = { name, fullPath: path.join(iconsDir, dir, file) };
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
matchComponents({
|
matchComponents(
|
||||||
"hero": ({name, fullPath}) => {
|
{
|
||||||
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
|
hero: ({ name, fullPath }) => {
|
||||||
content = encodeURIComponent(content)
|
let content = fs
|
||||||
let size = theme("spacing.6")
|
.readFileSync(fullPath)
|
||||||
|
.toString()
|
||||||
|
.replace(/\r?\n|\r/g, "");
|
||||||
|
content = encodeURIComponent(content);
|
||||||
|
let size = theme("spacing.6");
|
||||||
if (name.endsWith("-mini")) {
|
if (name.endsWith("-mini")) {
|
||||||
size = theme("spacing.5")
|
size = theme("spacing.5");
|
||||||
} else if (name.endsWith("-micro")) {
|
} else if (name.endsWith("-micro")) {
|
||||||
size = theme("spacing.4")
|
size = theme("spacing.4");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
|
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
|
||||||
"-webkit-mask": `var(--hero-${name})`,
|
"-webkit-mask": `var(--hero-${name})`,
|
||||||
"mask": `var(--hero-${name})`,
|
mask: `var(--hero-${name})`,
|
||||||
"mask-repeat": "no-repeat",
|
"mask-repeat": "no-repeat",
|
||||||
"background-color": "currentColor",
|
"background-color": "currentColor",
|
||||||
"vertical-align": "middle",
|
"vertical-align": "middle",
|
||||||
"display": "inline-block",
|
display: "inline-block",
|
||||||
"width": size,
|
width: size,
|
||||||
"height": size
|
height: size,
|
||||||
}
|
};
|
||||||
}
|
},
|
||||||
}, {values})
|
},
|
||||||
})
|
{ values },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -41,15 +41,11 @@
|
|||||||
ctx.shadowColor = options.shadowColor;
|
ctx.shadowColor = options.shadowColor;
|
||||||
|
|
||||||
var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
||||||
for (var stop in options.barColors)
|
for (var stop in options.barColors) lineGradient.addColorStop(stop, options.barColors[stop]);
|
||||||
lineGradient.addColorStop(stop, options.barColors[stop]);
|
|
||||||
ctx.lineWidth = options.barThickness;
|
ctx.lineWidth = options.barThickness;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(0, options.barThickness / 2);
|
ctx.moveTo(0, options.barThickness / 2);
|
||||||
ctx.lineTo(
|
ctx.lineTo(Math.ceil(currentProgress * canvas.width), options.barThickness / 2);
|
||||||
Math.ceil(currentProgress * canvas.width),
|
|
||||||
options.barThickness / 2
|
|
||||||
);
|
|
||||||
ctx.strokeStyle = lineGradient;
|
ctx.strokeStyle = lineGradient;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
},
|
},
|
||||||
@@ -65,8 +61,7 @@
|
|||||||
},
|
},
|
||||||
topbar = {
|
topbar = {
|
||||||
config: function (opts) {
|
config: function (opts) {
|
||||||
for (var key in opts)
|
for (var key in opts) if (options.hasOwnProperty(key)) options[key] = opts[key];
|
||||||
if (options.hasOwnProperty(key)) options[key] = opts[key];
|
|
||||||
},
|
},
|
||||||
show: function (delay) {
|
show: function (delay) {
|
||||||
if (showing) return;
|
if (showing) return;
|
||||||
@@ -84,9 +79,7 @@
|
|||||||
if (options.autoRun) {
|
if (options.autoRun) {
|
||||||
(function loop() {
|
(function loop() {
|
||||||
progressTimerId = window.requestAnimationFrame(loop);
|
progressTimerId = window.requestAnimationFrame(loop);
|
||||||
topbar.progress(
|
topbar.progress("+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2));
|
||||||
"+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
|
|
||||||
);
|
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,9 +88,7 @@
|
|||||||
if (typeof to === "undefined") return currentProgress;
|
if (typeof to === "undefined") return currentProgress;
|
||||||
if (typeof to === "string") {
|
if (typeof to === "string") {
|
||||||
to =
|
to =
|
||||||
(to.indexOf("+") >= 0 || to.indexOf("-") >= 0
|
(to.indexOf("+") >= 0 || to.indexOf("-") >= 0 ? currentProgress : 0) + parseFloat(to);
|
||||||
? currentProgress
|
|
||||||
: 0) + parseFloat(to);
|
|
||||||
}
|
}
|
||||||
currentProgress = to > 1 ? 1 : to;
|
currentProgress = to > 1 ? 1 : to;
|
||||||
repaint();
|
repaint();
|
||||||
@@ -135,4 +126,4 @@
|
|||||||
} else {
|
} else {
|
||||||
this.topbar = topbar;
|
this.topbar = topbar;
|
||||||
}
|
}
|
||||||
}.call(this, window, document));
|
}).call(this, window, document);
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ defmodule Chesstrainer.Application do
|
|||||||
# Start a worker by calling: Chesstrainer.Worker.start_link(arg)
|
# Start a worker by calling: Chesstrainer.Worker.start_link(arg)
|
||||||
# {Chesstrainer.Worker, arg},
|
# {Chesstrainer.Worker, arg},
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry
|
||||||
ChesstrainerWeb.Endpoint
|
ChesstrainerWeb.Endpoint,
|
||||||
|
{Registry, keys: :unique, name: Chesstrainer.CacheRegistry},
|
||||||
|
{Chesstrainer.Cache, :tablebase}
|
||||||
]
|
]
|
||||||
|
|
||||||
# See https://elixir.hexdocs.pm/Supervisor.html
|
# See https://elixir.hexdocs.pm/Supervisor.html
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
defmodule Chesstrainer.Cache do
|
||||||
|
@moduledoc """
|
||||||
|
Generic LRU cache.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> start_link(:tablebase)
|
||||||
|
"""
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
@max_size 1000
|
||||||
|
@backup_interval_minutes 60
|
||||||
|
|
||||||
|
def start_link(table), do: GenServer.start_link(__MODULE__, table, name: via_tuple(table))
|
||||||
|
def put(table, key, value), do: GenServer.call(via_tuple(table), {:put, key, value})
|
||||||
|
def get(table, key), do: GenServer.call(via_tuple(table), {:get, key})
|
||||||
|
def delete(table, key), do: GenServer.call(via_tuple(table), {:delete, key})
|
||||||
|
def backup(table), do: GenServer.cast(via_tuple(table), :backup)
|
||||||
|
|
||||||
|
def show(table) do
|
||||||
|
ensure_table(table)
|
||||||
|
:ets.tab2list(table)
|
||||||
|
end
|
||||||
|
|
||||||
|
def size(table), do: :ets.info(table, :size)
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(table) do
|
||||||
|
{:ok, table, {:continue, :restore_and_backup}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_continue(:restore_and_backup, table) do
|
||||||
|
create_table(table)
|
||||||
|
__MODULE__.Persist.restore(table)
|
||||||
|
:timer.send_interval(:timer.minutes(@backup_interval_minutes), :backup)
|
||||||
|
{:noreply, table}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:put, key, value}, _from, table) do
|
||||||
|
ensure_table(table)
|
||||||
|
timestamp = System.system_time(:millisecond)
|
||||||
|
:ets.insert(table, {key, value, timestamp})
|
||||||
|
|
||||||
|
GenServer.cast(self(), :maybe_evict)
|
||||||
|
|
||||||
|
{:reply, :ok, table}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:get, key}, _from, table) do
|
||||||
|
ensure_table(table)
|
||||||
|
|
||||||
|
case :ets.lookup(table, key) do
|
||||||
|
[{^key, value, _old_timestamp}] ->
|
||||||
|
timestamp = System.system_time(:millisecond)
|
||||||
|
:ets.insert(table, {key, value, timestamp})
|
||||||
|
{:reply, {:ok, value}, table}
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
{:reply, :error, table}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:delete, key}, _from, table) do
|
||||||
|
:ets.delete(table, key)
|
||||||
|
{:reply, :ok, table}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast(:maybe_evict, table) do
|
||||||
|
__MODULE__.Evict.maybe_evict_oldest(table, @max_size)
|
||||||
|
{:noreply, table}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast(:backup, table) do
|
||||||
|
__MODULE__.Persist.backup(table)
|
||||||
|
{:noreply, table}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:backup, table) do
|
||||||
|
GenServer.cast(self(), :backup)
|
||||||
|
{:noreply, table}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp via_tuple(table), do: {:via, Registry, {Chesstrainer.CacheRegistry, table}}
|
||||||
|
|
||||||
|
defp ensure_table(table) do
|
||||||
|
case :ets.whereis(table) do
|
||||||
|
:undefined -> create_table(table)
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_table(table), do: :ets.new(table, [:named_table, :public, :set])
|
||||||
|
end
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
defmodule Chesstrainer.Cache.Evict do
|
||||||
|
@moduledoc """
|
||||||
|
Eviction functions for LRU cache
|
||||||
|
"""
|
||||||
|
|
||||||
|
# O(n) time, not efficient
|
||||||
|
def maybe_evict_oldest(table, max_size) do
|
||||||
|
if :ets.info(table, :size) > max_size do
|
||||||
|
case find_oldest_entry(table) do
|
||||||
|
{oldest_key, _} -> :ets.delete(table, oldest_key)
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp find_oldest_entry(table) do
|
||||||
|
:ets.foldl(
|
||||||
|
fn {k, _v, t}, acc ->
|
||||||
|
case acc do
|
||||||
|
nil -> {k, t}
|
||||||
|
{_, oldest_t} when t < oldest_t -> {k, t}
|
||||||
|
_ -> acc
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
nil,
|
||||||
|
table
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
defmodule Chesstrainer.Cache.Persist do
|
||||||
|
@moduledoc """
|
||||||
|
Persistance for cache. Backup and restore functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@backup_path "priv/static/backups"
|
||||||
|
|
||||||
|
def backup(table) do
|
||||||
|
File.mkdir_p!(@backup_path)
|
||||||
|
filename = Path.join(@backup_path, "#{table}.det") |> String.to_charlist()
|
||||||
|
{:ok, dets_table} = :dets.open_file(filename, type: :set)
|
||||||
|
:ets.to_dets(table, dets_table)
|
||||||
|
:dets.sync(dets_table)
|
||||||
|
end
|
||||||
|
|
||||||
|
def restore(table) do
|
||||||
|
File.mkdir_p!(@backup_path)
|
||||||
|
filename = Path.join(@backup_path, "#{table}.det") |> String.to_charlist()
|
||||||
|
|
||||||
|
case :dets.open_file(table, file: filename, type: :set) do
|
||||||
|
{:ok, ^table} ->
|
||||||
|
:dets.foldl(fn entry, _ -> :ets.insert(table, entry) end, :ok, table)
|
||||||
|
:dets.close(table)
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
defmodule Chesstrainer.Endgames do
|
||||||
|
@moduledoc """
|
||||||
|
The Endgames context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
alias Chesstrainer.Repo
|
||||||
|
|
||||||
|
alias Chesstrainer.Endgames.Endgame
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the list of endgames.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> list_endgames()
|
||||||
|
[%Endgame{}, ...]
|
||||||
|
|
||||||
|
"""
|
||||||
|
def list_endgames do
|
||||||
|
Repo.all(Endgame)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets a single endgame.
|
||||||
|
|
||||||
|
Raises `Ecto.NoResultsError` if the Endgame does not exist.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> get_endgame!(123)
|
||||||
|
%Endgame{}
|
||||||
|
|
||||||
|
iex> get_endgame!(456)
|
||||||
|
** (Ecto.NoResultsError)
|
||||||
|
|
||||||
|
"""
|
||||||
|
def get_endgame!(id), do: Repo.get!(Endgame, id)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a endgame.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> create_endgame(%{field: value})
|
||||||
|
{:ok, %Endgame{}}
|
||||||
|
|
||||||
|
iex> create_endgame(%{field: bad_value})
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def create_endgame(attrs) do
|
||||||
|
%Endgame{}
|
||||||
|
|> Endgame.changeset(attrs)
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates a endgame.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> update_endgame(endgame, %{field: new_value})
|
||||||
|
{:ok, %Endgame{}}
|
||||||
|
|
||||||
|
iex> update_endgame(endgame, %{field: bad_value})
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def update_endgame(%Endgame{} = endgame, attrs) do
|
||||||
|
# ignore fen on update
|
||||||
|
attrs = Map.delete(attrs, "fen")
|
||||||
|
|
||||||
|
endgame
|
||||||
|
|> Endgame.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Deletes a endgame.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> delete_endgame(endgame)
|
||||||
|
{:ok, %Endgame{}}
|
||||||
|
|
||||||
|
iex> delete_endgame(endgame)
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def delete_endgame(%Endgame{} = endgame) do
|
||||||
|
Repo.delete(endgame)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns an `%Ecto.Changeset{}` for tracking endgame changes.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> change_endgame(endgame)
|
||||||
|
%Ecto.Changeset{data: %Endgame{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def change_endgame(%Endgame{} = endgame, attrs \\ %{}) do
|
||||||
|
Endgame.changeset(endgame, attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_tag(endgame, tag) do
|
||||||
|
endgame
|
||||||
|
|> Repo.preload(:tags)
|
||||||
|
|> Ecto.Changeset.change()
|
||||||
|
|> Ecto.Changeset.put_assoc(:tags, [tag | endgame.tags])
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_tag(endgame, tag) do
|
||||||
|
endgame
|
||||||
|
|> Repo.preload(:tags)
|
||||||
|
|> Ecto.Changeset.change()
|
||||||
|
|> Ecto.Changeset.put_assoc(:tags, endgame.tags -- [tag])
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
defmodule Chesstrainer.Endgames.Endgame do
|
||||||
|
@moduledoc """
|
||||||
|
A singular endgame entry
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
|
@foreign_key_type :binary_id
|
||||||
|
schema "endgames" do
|
||||||
|
field :fen, :string
|
||||||
|
field :color, Ecto.Enum, values: [:white, :black]
|
||||||
|
field :key, :string
|
||||||
|
field :message, :string
|
||||||
|
field :notes, :string
|
||||||
|
field :result, Ecto.Enum, values: [:win, :loss, :draw]
|
||||||
|
field :rating, :integer
|
||||||
|
field :rating_deviation, :integer
|
||||||
|
field :times_attempted, :integer
|
||||||
|
field :times_solved, :integer
|
||||||
|
|
||||||
|
many_to_many :tags, Chesstrainer.Tags.Tag,
|
||||||
|
join_through: "endgames_tags",
|
||||||
|
on_replace: :delete
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(endgame, attrs) do
|
||||||
|
endgame
|
||||||
|
|> cast(attrs, [:fen, :key, :message, :notes, :result, :rating, :color])
|
||||||
|
|> validate_required([:fen, :color], message: "Invalid FEN")
|
||||||
|
|> validate_required([:key, :result, :rating])
|
||||||
|
|> validate_format(:key, ~r/^(?=.{5,10}$)KQ*R*[NB]*P*\sv\sKQ*R*[NB]*P*$/,
|
||||||
|
message: "Key must follow pattern KQ.. v KQR.., max pieces 7"
|
||||||
|
)
|
||||||
|
|> unique_constraint(:fen, message: "FEN already exists")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
defmodule Chesstrainer.Endgames.Tablebase do
|
||||||
|
@moduledoc """
|
||||||
|
All endgame tablebase related functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Chesstrainer.Endgames.Tablebase
|
||||||
|
|
||||||
|
@type result :: :win | :draw | :loss | :unknown
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
category: result,
|
||||||
|
checkmate: boolean(),
|
||||||
|
dtc: integer() | nil,
|
||||||
|
dtm: integer() | nil,
|
||||||
|
dtw: integer() | nil,
|
||||||
|
dtz: integer() | nil,
|
||||||
|
insufficient_material: boolean(),
|
||||||
|
moves: [Tablebase.Move.t()]
|
||||||
|
}
|
||||||
|
|
||||||
|
defstruct category: nil,
|
||||||
|
checkmate: false,
|
||||||
|
dtc: nil,
|
||||||
|
dtm: nil,
|
||||||
|
dtw: nil,
|
||||||
|
dtz: nil,
|
||||||
|
insufficient_material: false,
|
||||||
|
moves: nil
|
||||||
|
|
||||||
|
@spec new(String.t()) ::
|
||||||
|
{:ok, t()}
|
||||||
|
| {:cooldown, non_neg_integer()}
|
||||||
|
| {:error, :invalid_endgame}
|
||||||
|
| {:error, term()}
|
||||||
|
def new(fen) do
|
||||||
|
case Tablebase.Cache.get(fen) do
|
||||||
|
{_fen, tablebase} ->
|
||||||
|
{:cache, tablebase}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
case Tablebase.Parser.from_fen(fen) do
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
|
||||||
|
{:cooldown, remaining_ms} ->
|
||||||
|
{:cooldown, remaining_ms}
|
||||||
|
|
||||||
|
{:ok, tablebase} when tablebase.category == :unknown ->
|
||||||
|
{:error, :invalid_endgame}
|
||||||
|
|
||||||
|
{:ok, tablebase} ->
|
||||||
|
Tablebase.Cache.put(fen, tablebase)
|
||||||
|
{:ok, tablebase}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
defmodule Chesstrainer.Endgames.Tablebase.Cache do
|
||||||
|
@moduledoc """
|
||||||
|
A cache of tablebases by FEN.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Chesstrainer.Cache
|
||||||
|
|
||||||
|
@table :tablebase
|
||||||
|
|
||||||
|
def put(fen, tablebase), do: Cache.put(@table, fen, tablebase)
|
||||||
|
|
||||||
|
def get(fen), do: Cache.get(@table, fen)
|
||||||
|
|
||||||
|
def delete(fen), do: Cache.delete(@table, fen)
|
||||||
|
def show, do: Cache.show(@table)
|
||||||
|
def size, do: Cache.size(@table)
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
defmodule Chesstrainer.Endgames.Tablebase.Move do
|
||||||
|
@moduledoc """
|
||||||
|
Moves struct for the tablebase
|
||||||
|
"""
|
||||||
|
|
||||||
|
@type result :: :win | :draw | :loss
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
category: result,
|
||||||
|
checkmate: boolean(),
|
||||||
|
conversion: boolean(),
|
||||||
|
dtc: integer() | nil,
|
||||||
|
dtm: integer() | nil,
|
||||||
|
dtw: integer() | nil,
|
||||||
|
dtz: integer() | nil,
|
||||||
|
precise_dtz: integer() | nil,
|
||||||
|
san: String.t() | nil,
|
||||||
|
stalemate: boolean(),
|
||||||
|
uci: String.t() | nil,
|
||||||
|
variant_loss: boolean(),
|
||||||
|
variant_win: boolean(),
|
||||||
|
zeroing: boolean(),
|
||||||
|
insufficient_material: boolean()
|
||||||
|
}
|
||||||
|
|
||||||
|
defstruct category: nil,
|
||||||
|
checkmate: false,
|
||||||
|
conversion: false,
|
||||||
|
dtc: nil,
|
||||||
|
dtm: nil,
|
||||||
|
dtw: nil,
|
||||||
|
dtz: nil,
|
||||||
|
precise_dtz: nil,
|
||||||
|
san: nil,
|
||||||
|
stalemate: false,
|
||||||
|
uci: nil,
|
||||||
|
variant_loss: false,
|
||||||
|
variant_win: false,
|
||||||
|
zeroing: false,
|
||||||
|
insufficient_material: false
|
||||||
|
end
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
defmodule Chesstrainer.Endgames.Tablebase.Parser do
|
||||||
|
@moduledoc """
|
||||||
|
Fetch and parse tablebase data to fit the tablebase struct
|
||||||
|
|
||||||
|
Lichess tablebase response from FEN string.
|
||||||
|
https://lichess.org/api#tag/tablebase
|
||||||
|
|
||||||
|
FEN must have spaces replaced by underscores to satisfy the Lichess API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Chesstrainer.Ratelimiters
|
||||||
|
alias Chesstrainer.Endgames.Tablebase
|
||||||
|
|
||||||
|
@lichess_tablebase_url "http://tablebase.lichess.ovh/standard?fen="
|
||||||
|
|
||||||
|
@spec from_fen(String.t()) ::
|
||||||
|
{:ok, Tablebase.t()} | {:cooldown, non_neg_integer()} | {:error, term()}
|
||||||
|
def from_fen(fen) do
|
||||||
|
case lichess_check_cooldown() do
|
||||||
|
{:ok, 0} -> tablebase_response(fen)
|
||||||
|
{:cooldown, remaining_ms} -> {:cooldown, remaining_ms}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec tablebase_response(String.t()) :: {:ok, Tablebase.t()} | {:error, term()}
|
||||||
|
defp tablebase_response(fen) do
|
||||||
|
response =
|
||||||
|
fen
|
||||||
|
|> sanitise_fen()
|
||||||
|
|> build_request_string()
|
||||||
|
|> Req.get()
|
||||||
|
|> parse_response()
|
||||||
|
|
||||||
|
case response do
|
||||||
|
{:ok, body} -> from_map(body)
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# exposed for testing
|
||||||
|
def from_map(map) do
|
||||||
|
{:ok,
|
||||||
|
%Tablebase{
|
||||||
|
category: map["category"] |> String.to_atom(),
|
||||||
|
checkmate: map["checkmate"],
|
||||||
|
dtc: map["dtc"],
|
||||||
|
dtm: map["dtm"],
|
||||||
|
dtw: map["dtw"],
|
||||||
|
dtz: map["dtz"],
|
||||||
|
insufficient_material: map["insufficient_material"],
|
||||||
|
moves: Enum.map(map["moves"], &moves_from_map/1)
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp moves_from_map(map) do
|
||||||
|
%Tablebase.Move{
|
||||||
|
category: map["category"] |> String.to_atom(),
|
||||||
|
checkmate: map["checkmate"],
|
||||||
|
conversion: map["conversion"],
|
||||||
|
dtc: map["dtc"],
|
||||||
|
dtm: map["dtm"],
|
||||||
|
dtw: map["dtw"],
|
||||||
|
dtz: map["dtz"],
|
||||||
|
insufficient_material: map["insufficient_material"],
|
||||||
|
precise_dtz: map["precise_dtz"],
|
||||||
|
san: map["san"],
|
||||||
|
stalemate: map["stalemate"],
|
||||||
|
uci: map["uci"],
|
||||||
|
variant_loss: map["variant_loss"],
|
||||||
|
variant_win: map["variant_win"],
|
||||||
|
zeroing: map["zeroing"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_response(response) do
|
||||||
|
case response do
|
||||||
|
# Success
|
||||||
|
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||||
|
{:ok, body}
|
||||||
|
|
||||||
|
# Client errors
|
||||||
|
{:ok, %Req.Response{status: status, body: body}} when status in 400..499 ->
|
||||||
|
{:error, {:client_error, status, body}}
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
{:ok, %Req.Response{status: 429, body: body}} ->
|
||||||
|
lichess_add_cooldown()
|
||||||
|
{:error, {:too_many_requests, 429, body}}
|
||||||
|
|
||||||
|
# Server errors
|
||||||
|
{:ok, %Req.Response{status: status, body: body}} when status in 500..599 ->
|
||||||
|
{:error, {:server_error, status, body}}
|
||||||
|
|
||||||
|
# Catch‑all for other status codes
|
||||||
|
{:ok, %Req.Response{status: status, body: body}} ->
|
||||||
|
{:error, {:http_error, status, body}}
|
||||||
|
|
||||||
|
# Network / client‑side failure
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sanitise_fen(fen), do: String.replace(fen, " ", "_")
|
||||||
|
defp build_request_string(fen), do: "#{@lichess_tablebase_url}#{fen}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
defmodule Chesstrainer.FEN do
|
||||||
|
@spec color_from_fen(String.t()) :: :white | :black | nil
|
||||||
|
def color_from_fen(fen) do
|
||||||
|
fen
|
||||||
|
|> String.split(" ")
|
||||||
|
|> Enum.at(1)
|
||||||
|
|> color_initial_to_color_atom()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp color_initial_to_color_atom("b"), do: :black
|
||||||
|
defp color_initial_to_color_atom("w"), do: :white
|
||||||
|
defp color_initial_to_color_atom(_), do: nil
|
||||||
|
end
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
defmodule Chesstrainer.Game do
|
||||||
|
@moduledoc """
|
||||||
|
Game related functions
|
||||||
|
"""
|
||||||
|
alias Chesstrainer.Endgames
|
||||||
|
|
||||||
|
@type game_type :: :endgame
|
||||||
|
@type square :: {atom(), pos_integer()}
|
||||||
|
@type move :: {square, square}
|
||||||
|
@type colors :: :white | :black
|
||||||
|
@type game_state :: :continue | :loss | :draw
|
||||||
|
@type result :: :win | :draw | :loss
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
player_color: colors,
|
||||||
|
board: map(),
|
||||||
|
active_color: colors,
|
||||||
|
castling: list(),
|
||||||
|
en_passant: term() | nil,
|
||||||
|
moves: list(),
|
||||||
|
halfmove_clock: non_neg_integer(),
|
||||||
|
fullmove_clock: non_neg_integer(),
|
||||||
|
captures: list(),
|
||||||
|
check: term() | nil,
|
||||||
|
result: result,
|
||||||
|
pgn: String.t() | nil,
|
||||||
|
orientation: colors,
|
||||||
|
move_from_square: square,
|
||||||
|
move_to_square: square,
|
||||||
|
game_type: game_type,
|
||||||
|
fen: String.t(),
|
||||||
|
tablebase: term(),
|
||||||
|
game_state: game_state
|
||||||
|
}
|
||||||
|
|
||||||
|
defstruct board: %{},
|
||||||
|
player_color: :white,
|
||||||
|
active_color: :white,
|
||||||
|
castling: [],
|
||||||
|
en_passant: nil,
|
||||||
|
moves: [],
|
||||||
|
halfmove_clock: 0,
|
||||||
|
fullmove_clock: 0,
|
||||||
|
captures: [],
|
||||||
|
check: nil,
|
||||||
|
result: nil,
|
||||||
|
pgn: nil,
|
||||||
|
orientation: :white,
|
||||||
|
move_from_square: nil,
|
||||||
|
move_to_square: nil,
|
||||||
|
game_type: nil,
|
||||||
|
fen: nil,
|
||||||
|
tablebase: nil,
|
||||||
|
game_state: :continue
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Builds a `%ChessTrainer.Game{}` from a FEN string.
|
||||||
|
|
||||||
|
On success, returns `{:ok, game}` with the parsed board state.
|
||||||
|
|
||||||
|
If the FEN string is invalid, `Chex.Parser.FEN.parse/1` raises a
|
||||||
|
`MatchError`. This function rescues that exception and returns
|
||||||
|
`{:error, :invalid_fen, game}`, where `game` is a fallback position
|
||||||
|
containing only kings.
|
||||||
|
|
||||||
|
This is a bad implementation in the Chex libary and should be fixed at our fork:
|
||||||
|
https://github.com/therealowenrees/chex
|
||||||
|
|
||||||
|
We should return something like{:ok, game} | {:error | :invalid_fen}
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> ChessTrainer.Game.game_from_fen("valid fen", :endgame)
|
||||||
|
{:ok, %ChessTrainer.Game{}}
|
||||||
|
|
||||||
|
iex> ChessTrainer.Game.game_from_fen("invalid fen", :endgame)
|
||||||
|
{:error, :invalid_fen, %ChessTrainer.Game{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
@dialyzer {:nowarn_function, game_from_fen: 2}
|
||||||
|
@spec game_from_fen(String.t(), game_type()) :: {:ok, t()} | {:error, :invalid_fen, t()}
|
||||||
|
def game_from_fen(fen, game_type) do
|
||||||
|
try do
|
||||||
|
{:ok, %Chex.Game{} = chex_game} = Chex.Parser.FEN.parse(fen)
|
||||||
|
|
||||||
|
game = %__MODULE__{
|
||||||
|
board: chex_game.board,
|
||||||
|
player_color: chex_game.active_color,
|
||||||
|
active_color: chex_game.active_color,
|
||||||
|
castling: chex_game.castling,
|
||||||
|
en_passant: chex_game.en_passant,
|
||||||
|
moves: chex_game.moves,
|
||||||
|
halfmove_clock: chex_game.halfmove_clock,
|
||||||
|
fullmove_clock: chex_game.fullmove_clock,
|
||||||
|
captures: chex_game.captures,
|
||||||
|
check: chex_game.check,
|
||||||
|
result: chex_game.result,
|
||||||
|
pgn: chex_game.pgn,
|
||||||
|
orientation: chex_game.active_color,
|
||||||
|
move_from_square: nil,
|
||||||
|
move_to_square: nil,
|
||||||
|
game_type: game_type,
|
||||||
|
fen: fen,
|
||||||
|
game_state: :continue,
|
||||||
|
tablebase:
|
||||||
|
if game_type === :endgame do
|
||||||
|
Endgames.Tablebase.new(fen)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, game}
|
||||||
|
rescue
|
||||||
|
# if a MatchError error is raised, then return a game with only kings on the board
|
||||||
|
MatchError ->
|
||||||
|
game = %__MODULE__{
|
||||||
|
board: %{
|
||||||
|
{:e, 1} => {:king, :white, {:e, 1}},
|
||||||
|
{:e, 8} => {:king, :black, {:e, 8}}
|
||||||
|
},
|
||||||
|
player_color: :white,
|
||||||
|
active_color: :white,
|
||||||
|
castling: [],
|
||||||
|
en_passant: nil,
|
||||||
|
moves: [],
|
||||||
|
halfmove_clock: 0,
|
||||||
|
fullmove_clock: 0,
|
||||||
|
captures: nil,
|
||||||
|
check: nil,
|
||||||
|
result: :draw,
|
||||||
|
pgn: nil,
|
||||||
|
orientation: :white,
|
||||||
|
move_from_square: nil,
|
||||||
|
move_to_square: nil,
|
||||||
|
game_type: :endgame,
|
||||||
|
fen: "4k3/8/8/8/8/8/8/4K3 w - - 0 1",
|
||||||
|
game_state: :draw,
|
||||||
|
tablebase: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
{:error, :invalid_fen, game}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
defmodule Chesstrainer.Ratelimiters do
|
||||||
|
@moduledoc """
|
||||||
|
Generic rate limiter for external APIs (e.g., Lichess).
|
||||||
|
|
||||||
|
This module exposes convenience functions for each external API
|
||||||
|
that delegate to their specific rate limiter implementation.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> ChessTrainer.Ratelimiters.lichess_reset_cooldown()
|
||||||
|
true
|
||||||
|
|
||||||
|
iex> ChessTrainer.Ratelimiters.lichess_check_cooldown()
|
||||||
|
{:ok, 0}
|
||||||
|
|
||||||
|
iex> ChessTrainer.Ratelimiters.lichess_add_cooldown()
|
||||||
|
true
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Chesstrainer.Ratelimiters.Lichess
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Adds a cooldown entry for the Lichess API.
|
||||||
|
|
||||||
|
This sets the cooldown period (e.g. 60 seconds) starting from now.
|
||||||
|
Useful when a `429 Too Many Requests` response is received.
|
||||||
|
"""
|
||||||
|
@spec lichess_add_cooldown() :: boolean()
|
||||||
|
def lichess_add_cooldown, do: Lichess.add_cooldown()
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Resets the cooldown for the Lichess API.
|
||||||
|
|
||||||
|
This clears any existing cooldown, allowing requests immediately.
|
||||||
|
"""
|
||||||
|
@spec lichess_reset_cooldown() :: boolean()
|
||||||
|
def lichess_reset_cooldown, do: Lichess.reset_cooldown()
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks whether the Lichess API is currently under cooldown.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* `{:ok, 0}` if no cooldown is active
|
||||||
|
* `{:cooldown, remaining_ms}` if still cooling down, with the remaining time in milliseconds
|
||||||
|
"""
|
||||||
|
@spec lichess_check_cooldown() :: {:ok, non_neg_integer} | {:cooldown, non_neg_integer}
|
||||||
|
def lichess_check_cooldown, do: Lichess.check_cooldown()
|
||||||
|
end
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
defmodule Chesstrainer.Ratelimiters.Lichess do
|
||||||
|
@moduledoc """
|
||||||
|
Global rate limiter for 429 response codes from the Lichess API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@cooldown_seconds 60
|
||||||
|
@table :lichess_cooldown
|
||||||
|
|
||||||
|
def add_cooldown do
|
||||||
|
ensure_table()
|
||||||
|
insert_cooldown()
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_cooldown do
|
||||||
|
ensure_table()
|
||||||
|
:ets.insert(@table, {:until, nil})
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_cooldown do
|
||||||
|
ensure_table()
|
||||||
|
current_ms = System.system_time(:millisecond)
|
||||||
|
|
||||||
|
case :ets.lookup(@table, :until) do
|
||||||
|
[{:until, nil}] ->
|
||||||
|
{:ok, 0}
|
||||||
|
|
||||||
|
[{:until, until_ms}] ->
|
||||||
|
remaining = until_ms - current_ms
|
||||||
|
|
||||||
|
if remaining <= 0 do
|
||||||
|
reset_cooldown()
|
||||||
|
{:ok, 0}
|
||||||
|
else
|
||||||
|
{:cooldown, remaining}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create do
|
||||||
|
:ets.new(@table, [:named_table, :public, :set])
|
||||||
|
:ets.insert(@table, {:until, nil})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ensure_table do
|
||||||
|
case :ets.whereis(@table) do
|
||||||
|
:undefined -> create()
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp insert_cooldown do
|
||||||
|
until_ms = System.system_time(:millisecond) + @cooldown_seconds * 1000
|
||||||
|
:ets.insert(@table, {:until, until_ms})
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
defmodule Chesstrainer.Tags do
|
||||||
|
@moduledoc """
|
||||||
|
The Tags context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
alias Chesstrainer.Repo
|
||||||
|
|
||||||
|
alias Chesstrainer.Tags.Tag
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the list of tags.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> list_tags()
|
||||||
|
[%Tag{}, ...]
|
||||||
|
|
||||||
|
"""
|
||||||
|
def list_tags do
|
||||||
|
Repo.all(Tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets a single tag.
|
||||||
|
|
||||||
|
Raises `Ecto.NoResultsError` if the Tag does not exist.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> get_tag!(123)
|
||||||
|
%Tag{}
|
||||||
|
|
||||||
|
iex> get_tag!(456)
|
||||||
|
** (Ecto.NoResultsError)
|
||||||
|
|
||||||
|
"""
|
||||||
|
def get_tag!(id), do: Repo.get!(Tag, id)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a tag.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> create_tag(%{field: value})
|
||||||
|
{:ok, %Tag{}}
|
||||||
|
|
||||||
|
iex> create_tag(%{field: bad_value})
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def create_tag(attrs) do
|
||||||
|
%Tag{}
|
||||||
|
|> Tag.changeset(attrs)
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates a tag.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> update_tag(tag, %{field: new_value})
|
||||||
|
{:ok, %Tag{}}
|
||||||
|
|
||||||
|
iex> update_tag(tag, %{field: bad_value})
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def update_tag(%Tag{} = tag, attrs) do
|
||||||
|
tag
|
||||||
|
|> Tag.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Deletes a tag.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> delete_tag(tag)
|
||||||
|
{:ok, %Tag{}}
|
||||||
|
|
||||||
|
iex> delete_tag(tag)
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def delete_tag(%Tag{} = tag) do
|
||||||
|
Repo.delete(tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns an `%Ecto.Changeset{}` for tracking tag changes.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> change_tag(tag)
|
||||||
|
%Ecto.Changeset{data: %Tag{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def change_tag(%Tag{} = tag, attrs \\ %{}) do
|
||||||
|
Tag.changeset(tag, attrs)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
defmodule Chesstrainer.Tags.Tag do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "tags" do
|
||||||
|
field :name, :string
|
||||||
|
field :category, Ecto.Enum, values: [endgame: "endgame", opening: "opening", tactic: "tactic"]
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(tag, attrs) do
|
||||||
|
tag
|
||||||
|
|> cast(attrs, [:name, :category])
|
||||||
|
|> validate_required([:name, :category])
|
||||||
|
|> unique_constraint(:name)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
defmodule ChesstrainerWeb.Board.BoardComponent do
|
||||||
|
@moduledoc """
|
||||||
|
Chess board component
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Phoenix.Component
|
||||||
|
import ChesstrainerWeb.Board.SquareComponent
|
||||||
|
|
||||||
|
attr :game, :map, required: true
|
||||||
|
|
||||||
|
attr :myself, :any, required: true
|
||||||
|
|
||||||
|
def board(assigns) do
|
||||||
|
files_list = [:a, :b, :c, :d, :e, :f, :g, :h]
|
||||||
|
|
||||||
|
assigns =
|
||||||
|
assigns
|
||||||
|
|> assign(:files_list, files_list)
|
||||||
|
|> assign(
|
||||||
|
:files,
|
||||||
|
case assigns.game.orientation do
|
||||||
|
:white -> files_list
|
||||||
|
:black -> Enum.reverse(files_list)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:ranks,
|
||||||
|
case assigns.game.orientation do
|
||||||
|
:white -> Enum.reverse(1..8)
|
||||||
|
:black -> 1..8
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div class="w-[388px] m-auto left-0 right-0 mt-6">
|
||||||
|
<div class="border border-2 border-zinc-700 grid grid-rows-8 grid-cols-8">
|
||||||
|
<%= for rank <- @ranks do %>
|
||||||
|
<%= for file <- @files do %>
|
||||||
|
<% square = {file, rank} %>
|
||||||
|
<% piece = Map.get(@game.board, square) %>
|
||||||
|
<.square
|
||||||
|
file={file}
|
||||||
|
rank={rank}
|
||||||
|
piece={piece}
|
||||||
|
files_list={@files_list}
|
||||||
|
move_from_square={@game.move_from_square}
|
||||||
|
myself={@myself}
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
defmodule ChesstrainerWeb.Board.PieceComponent do
|
||||||
|
@moduledoc """
|
||||||
|
Chess pieces component
|
||||||
|
"""
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
attr :piece, :any, required: true
|
||||||
|
attr :class, :string, default: ""
|
||||||
|
|
||||||
|
def piece(assigns) do
|
||||||
|
assigns = assign(assigns, :path, piece_path(assigns.piece))
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<%= if @path do %>
|
||||||
|
<img src={@path} class={@class} />
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def piece_path({:king, :white, _square}), do: "/images/pieces/white/king.svg"
|
||||||
|
def piece_path({:queen, :white, _square}), do: "/images/pieces/white/queen.svg"
|
||||||
|
def piece_path({:rook, :white, _square}), do: "/images/pieces/white/rook.svg"
|
||||||
|
def piece_path({:bishop, :white, _square}), do: "/images/pieces/white/bishop.svg"
|
||||||
|
def piece_path({:knight, :white, _square}), do: "/images/pieces/white/knight.svg"
|
||||||
|
def piece_path({:pawn, :white, _square}), do: "/images/pieces/white/pawn.svg"
|
||||||
|
def piece_path({:king, :black, _square}), do: "/images/pieces/black/king.svg"
|
||||||
|
def piece_path({:queen, :black, _square}), do: "/images/pieces/black/queen.svg"
|
||||||
|
def piece_path({:rook, :black, _square}), do: "/images/pieces/black/rook.svg"
|
||||||
|
def piece_path({:bishop, :black, _square}), do: "/images/pieces/black/bishop.svg"
|
||||||
|
def piece_path({:knight, :black, _square}), do: "/images/pieces/black/knight.svg"
|
||||||
|
def piece_path({:pawn, :black, _square}), do: "/images/pieces/black/pawn.svg"
|
||||||
|
def piece_path(_), do: nil
|
||||||
|
end
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
defmodule ChesstrainerWeb.Board.SquareComponent do
|
||||||
|
@moduledoc """
|
||||||
|
Renders a single chess square with optional piece and highlighting.
|
||||||
|
"""
|
||||||
|
use Phoenix.Component
|
||||||
|
import ChesstrainerWeb.Board.PieceComponent
|
||||||
|
|
||||||
|
attr :file, :atom, required: true
|
||||||
|
attr :rank, :integer, required: true
|
||||||
|
attr :piece, :any, default: nil
|
||||||
|
attr :files_list, :list, required: true
|
||||||
|
attr :move_from_square, :any, default: nil
|
||||||
|
attr :myself, :any, required: true
|
||||||
|
|
||||||
|
def square(assigns) do
|
||||||
|
current_square = {assigns.file, assigns.rank}
|
||||||
|
|
||||||
|
assigns =
|
||||||
|
assigns
|
||||||
|
|> assign(:highlight_square, current_square === assigns.move_from_square)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
"w-12 h-12 flex items-center justify-center",
|
||||||
|
background(
|
||||||
|
Enum.find_index(@files_list, fn f -> f == @file end),
|
||||||
|
@rank - 1
|
||||||
|
),
|
||||||
|
highlight_square(@highlight_square)
|
||||||
|
]}
|
||||||
|
phx-click="square-click"
|
||||||
|
phx-value-file={@file}
|
||||||
|
phx-value-rank={@rank}
|
||||||
|
phx-value-type="move"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<.piece piece={@piece} class="w-10 h-10" />
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def background(file_idx, rank_idx) when rem(file_idx + rank_idx, 2) != 0, do: "bg-boardwhite"
|
||||||
|
def background(_, _), do: "bg-boardblack"
|
||||||
|
|
||||||
|
def highlight_square(true), do: "ring-inset ring-2 ring-yellow-400"
|
||||||
|
def highlight_square(false), do: ""
|
||||||
|
end
|
||||||
@@ -3,7 +3,7 @@ defmodule ChesstrainerWeb.ChessTestLive do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
{:ok, assign(socket, last_move: "None yet", total_moves: 0)}
|
{:ok, assign(socket, last_move: "None yet", san: "None yet", move_list: [])}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@@ -12,14 +12,36 @@ defmodule ChesstrainerWeb.ChessTestLive do
|
|||||||
<div class="mx-auto max-w-2xl p-6">
|
<div class="mx-auto max-w-2xl p-6">
|
||||||
<h1 class="text-2xl font-bold mb-4">Chessground LiveView Test</h1>
|
<h1 class="text-2xl font-bold mb-4">Chessground LiveView Test</h1>
|
||||||
|
|
||||||
|
<form phx-submit="load_fen" class="mb-6 flex gap-2 bg-gray-50 p-4 rounded-lg shadow-sm">
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="fen"
|
||||||
|
placeholder="Paste FEN string here (e.g., rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1)"
|
||||||
|
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
Load FEN
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="bg-gray-100 p-4 rounded mb-4 flex justify-between items-center shadow-sm">
|
<div class="bg-gray-100 p-4 rounded mb-4 flex justify-between items-center shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-gray-600 font-semibold">Last Played Move:</p>
|
<p class="text-sm text-gray-600 font-semibold">Last Played Move:</p>
|
||||||
<p class="text-lg font-mono text-blue-600">{@last_move}</p>
|
<p class="text-lg font-mono text-blue-600">{@last_move}</p>
|
||||||
</div>
|
<p class="text-lg font-mono text-blue-600">{@san}</p>
|
||||||
<div>
|
<p class="text-lg font-mono text-emerald-600">
|
||||||
<p class="text-sm text-gray-600 font-semibold">Total Moves:</p>
|
{if length(@move_list) == 0 do
|
||||||
<p class="text-lg font-bold">{@total_moves}</p>
|
"No Moves"
|
||||||
|
else
|
||||||
|
Enum.reverse(@move_list) |> Enum.join(", ")
|
||||||
|
end}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -37,11 +59,29 @@ defmodule ChesstrainerWeb.ChessTestLive do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("move_played", %{"from" => from, "to" => to, "fen" => _fen}, socket) do
|
def handle_event("load_fen", %{"fen" => fen}, socket) do
|
||||||
# Receive the payload sent from the client JS Hook via this.pushEvent
|
clean_fen = String.trim(fen)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:last_move, "Position Loaded")
|
||||||
|
|> assign(:san, "None yet")
|
||||||
|
|> assign(:move_list, [])
|
||||||
|
|> push_event("set_fen", %{fen: clean_fen})}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event(
|
||||||
|
"move_played",
|
||||||
|
%{"from" => from, "to" => to, "fen" => _fen, "san" => san},
|
||||||
|
socket
|
||||||
|
) do
|
||||||
|
current_moves = socket.assigns.move_list
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:last_move, "#{from} ➔ #{to}")
|
|> assign(:last_move, "#{from} ➔ #{to}")
|
||||||
|> assign(:total_moves, socket.assigns.total_moves + 1)}
|
|> assign(:san, san)
|
||||||
|
|> assign(:move_list, [san | current_moves])}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
defmodule ChesstrainerWeb.EndgameLive.Form do
|
||||||
|
use ChesstrainerWeb, :live_view
|
||||||
|
|
||||||
|
alias Chesstrainer.Repo
|
||||||
|
alias Chesstrainer.Tags
|
||||||
|
alias Chesstrainer.FEN
|
||||||
|
alias Chesstrainer.Endgames
|
||||||
|
alias Chesstrainer.Endgames.Endgame
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<Layouts.app flash={@flash}>
|
||||||
|
<.header>
|
||||||
|
{@page_title}
|
||||||
|
<:subtitle>Use this form to manage endgame records in your database.</:subtitle>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.form for={@form} id="endgame-form" phx-change="validate" phx-submit="save">
|
||||||
|
<.input field={@form[:fen]} type="text" label="Fen" disabled={@live_action == :edit} />
|
||||||
|
<.input field={@form[:color]} type="text" label="Color" disabled />
|
||||||
|
<.input field={@form[:key]} type="text" label="Key" />
|
||||||
|
<.input field={@form[:rating]} type="number" label="Rating" />
|
||||||
|
|
||||||
|
<!-- ✅ TAG SELECTOR -->
|
||||||
|
<div class="field">
|
||||||
|
<label>Tags</label>
|
||||||
|
|
||||||
|
<!-- Search box -->
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="tag_search"
|
||||||
|
value={@tag_query}
|
||||||
|
phx-change="search_tags"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Search results -->
|
||||||
|
<ul class="tag-results">
|
||||||
|
<%= for tag <- @tag_results do %>
|
||||||
|
<li phx-click="add_tag" phx-value-id={tag.id}>
|
||||||
|
{tag.name}
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if @tag_query != "" and @tag_results == [] do %>
|
||||||
|
<li phx-click="create_tag" phx-value-name={@tag_query}>
|
||||||
|
Create tag "{@tag_query}"
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Selected tags -->
|
||||||
|
<div class="selected-tags">
|
||||||
|
<%= for tag <- @endgame.tags do %>
|
||||||
|
<span class="tag">
|
||||||
|
{tag.name}
|
||||||
|
<button type="button" phx-click="remove_tag" phx-value-id={tag.id}>×</button>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<.input field={@form[:message]} type="textarea" label="Message" />
|
||||||
|
<.input field={@form[:notes]} type="textarea" label="Notes" />
|
||||||
|
<.input
|
||||||
|
field={@form[:result]}
|
||||||
|
type="select"
|
||||||
|
options={[
|
||||||
|
{"Win", :win},
|
||||||
|
{"Draw", :draw},
|
||||||
|
{"Loss", :loss}
|
||||||
|
]}
|
||||||
|
label="Result"
|
||||||
|
/>
|
||||||
|
<footer>
|
||||||
|
<.button phx-disable-with="Saving..." variant="primary">Save Endgame</.button>
|
||||||
|
<.button navigate={return_path(@return_to, @endgame)}>Cancel</.button>
|
||||||
|
</footer>
|
||||||
|
</.form>
|
||||||
|
</Layouts.app>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(params, _session, socket) do
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> assign(:return_to, return_to(params["return_to"]))
|
||||||
|
|> apply_action(socket.assigns.live_action, params)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp return_to("show"), do: "show"
|
||||||
|
defp return_to(_), do: "index"
|
||||||
|
|
||||||
|
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||||
|
endgame = Endgames.get_endgame!(id)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Edit Endgame")
|
||||||
|
|> assign(:endgame, endgame)
|
||||||
|
|> assign(:tag_query, "")
|
||||||
|
|> assign(:tag_results, [])
|
||||||
|
|> assign(:endgame, Repo.preload(endgame, :tags))
|
||||||
|
|> assign(:form, to_form(Endgames.change_endgame(endgame)))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_action(socket, :new, _params) do
|
||||||
|
endgame = %Endgame{}
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "New Endgame")
|
||||||
|
|> assign(:endgame, endgame)
|
||||||
|
|> assign(:tag_query, "")
|
||||||
|
|> assign(:tag_results, [])
|
||||||
|
|> assign(:endgame, Repo.preload(endgame, :tags))
|
||||||
|
|> assign(:form, to_form(Endgames.change_endgame(endgame)))
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("search_tags", %{"tag_search" => query}, socket) do
|
||||||
|
tags = Tags.search(query, "endgame")
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:tag_query, query)
|
||||||
|
|> assign(:tag_results, tags)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("add_tag", %{"id" => id}, socket) do
|
||||||
|
tag = Tags.get_tag!(id)
|
||||||
|
{:ok, endgame} = Endgames.add_tag(socket.assigns.endgame, tag)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:endgame, Repo.preload(endgame, :tags))
|
||||||
|
|> assign(:tag_query, "")
|
||||||
|
|> assign(:tag_results, [])}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("create_tag", %{"name" => name}, socket) do
|
||||||
|
{:ok, tag} = Tags.get_or_create(name, "endgame")
|
||||||
|
{:ok, endgame} = Endgames.add_tag(socket.assigns.endgame, tag)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:endgame, Repo.preload(endgame, :tags))
|
||||||
|
|> assign(:tag_query, "")
|
||||||
|
|> assign(:tag_results, [])}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("remove_tag", %{"id" => id}, socket) do
|
||||||
|
tag = Tags.get_tag!(id)
|
||||||
|
{:ok, endgame} = Endgames.remove_tag(socket.assigns.endgame, tag)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:endgame, Repo.preload(endgame, :tags))}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("validate", %{"endgame" => endgame_params}, socket) do
|
||||||
|
# Compute color from the incoming FEN
|
||||||
|
color = FEN.color_from_fen(endgame_params["fen"])
|
||||||
|
|
||||||
|
# Update the struct
|
||||||
|
endgame = %{socket.assigns.endgame | color: color}
|
||||||
|
|
||||||
|
# Build the changeset *from the updated struct*
|
||||||
|
changeset =
|
||||||
|
endgame
|
||||||
|
|> Endgames.change_endgame()
|
||||||
|
|> Ecto.Changeset.put_change(:color, color)
|
||||||
|
|
||||||
|
{:noreply, assign(socket, endgame: endgame, form: to_form(changeset, action: :validate))}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("save", %{"endgame" => endgame_params}, socket) do
|
||||||
|
save_endgame(socket, socket.assigns.live_action, endgame_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_endgame(socket, :edit, endgame_params) do
|
||||||
|
case Endgames.update_endgame(socket.assigns.endgame, endgame_params) do
|
||||||
|
{:ok, endgame} ->
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:info, "Endgame updated successfully")
|
||||||
|
|> push_navigate(to: return_path(socket.assigns.return_to, endgame))}
|
||||||
|
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
{:noreply, assign(socket, form: to_form(changeset))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_endgame(socket, :new, endgame_params) do
|
||||||
|
color = FEN.color_from_fen(endgame_params["fen"])
|
||||||
|
params = Map.put(endgame_params, "color", color)
|
||||||
|
|
||||||
|
case Endgames.create_endgame(params) do
|
||||||
|
{:ok, endgame} ->
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:info, "Endgame created successfully")
|
||||||
|
|> push_navigate(to: return_path(socket.assigns.return_to, endgame))}
|
||||||
|
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
{:noreply, assign(socket, form: to_form(changeset))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp return_path("index", _endgame), do: ~p"/admin/endgames"
|
||||||
|
defp return_path("show", endgame), do: ~p"/admin/endgames/#{endgame}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
defmodule ChesstrainerWeb.EndgameLive.Index do
|
||||||
|
use ChesstrainerWeb, :live_view
|
||||||
|
|
||||||
|
alias Chesstrainer.Endgames
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<Layouts.app flash={@flash}>
|
||||||
|
<.header>
|
||||||
|
Listing Endgames
|
||||||
|
<:actions>
|
||||||
|
<.button variant="primary" navigate={~p"/admin/endgames/new"}>
|
||||||
|
<.icon name="hero-plus" /> New Endgame
|
||||||
|
</.button>
|
||||||
|
</:actions>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.table
|
||||||
|
id="endgames"
|
||||||
|
rows={@streams.endgames}
|
||||||
|
row_click={fn {_id, endgame} -> JS.navigate(~p"/admin/endgames/#{endgame}") end}
|
||||||
|
>
|
||||||
|
<:col :let={{_id, endgame}} label="Fen">{endgame.fen}</:col>
|
||||||
|
<:col :let={{_id, endgame}} label="Color">{endgame.color}</:col>
|
||||||
|
<:col :let={{_id, endgame}} label="Key">{endgame.key}</:col>
|
||||||
|
<:col :let={{_id, endgame}} label="Rating">{endgame.rating}</:col>
|
||||||
|
<:col :let={{_id, endgame}} label="Message">{endgame.message}</:col>
|
||||||
|
<:col :let={{_id, endgame}} label="Notes">{endgame.notes}</:col>
|
||||||
|
<:col :let={{_id, endgame}} label="Result">{endgame.result}</:col>
|
||||||
|
<:col :let={{_id, endgame}} label="Attempted">{endgame.times_attempted}</:col>
|
||||||
|
<:col :let={{_id, endgame}} label="Solved">{endgame.times_solved}</:col>
|
||||||
|
<:action :let={{_id, endgame}}>
|
||||||
|
<div class="sr-only">
|
||||||
|
<.link navigate={~p"/admin/endgames/#{endgame}"}>Show</.link>
|
||||||
|
</div>
|
||||||
|
<.link navigate={~p"/admin/endgames/#{endgame}/edit"}>Edit</.link>
|
||||||
|
</:action>
|
||||||
|
<:action :let={{id, endgame}}>
|
||||||
|
<.link
|
||||||
|
phx-click={JS.push("delete", value: %{id: endgame.id}) |> hide("##{id}")}
|
||||||
|
data-confirm="Are you sure?"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</.link>
|
||||||
|
</:action>
|
||||||
|
</.table>
|
||||||
|
</Layouts.app>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Listing Endgames")
|
||||||
|
|> stream(:endgames, list_endgames())}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
|
endgame = Endgames.get_endgame!(id)
|
||||||
|
{:ok, _} = Endgames.delete_endgame(endgame)
|
||||||
|
|
||||||
|
{:noreply, stream_delete(socket, :endgames, endgame)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp list_endgames() do
|
||||||
|
Endgames.list_endgames()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
defmodule ChesstrainerWeb.EndgameLive.Show do
|
||||||
|
use ChesstrainerWeb, :live_view
|
||||||
|
|
||||||
|
alias Chesstrainer.Endgames
|
||||||
|
alias Chesstrainer.Game
|
||||||
|
|
||||||
|
import ChesstrainerWeb.Board.BoardComponent
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
game =
|
||||||
|
case Game.game_from_fen(assigns.endgame.fen, :endgame) do
|
||||||
|
{:ok, g} -> g
|
||||||
|
{:error, :invalid_fen, g} -> g
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO once we return {:error, :invalid_fen, game}, then put flash to show it is invalid
|
||||||
|
# write tests for this
|
||||||
|
|
||||||
|
assigns =
|
||||||
|
assigns
|
||||||
|
|> assign(:game, game)
|
||||||
|
|> assign(:game_type, :endgame)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<Layouts.app flash={@flash}>
|
||||||
|
<.header>
|
||||||
|
Endgame {@endgame.id}
|
||||||
|
<:subtitle>This is a endgame record from your database.</:subtitle>
|
||||||
|
<:actions>
|
||||||
|
<.button navigate={~p"/admin/endgames"}>
|
||||||
|
<.icon name="hero-arrow-left" />
|
||||||
|
</.button>
|
||||||
|
<.button variant="primary" navigate={~p"/admin/endgames/#{@endgame}/edit?return_to=show"}>
|
||||||
|
<.icon name="hero-pencil-square" /> Edit endgame
|
||||||
|
</.button>
|
||||||
|
</:actions>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.board game={@game} myself="board" />
|
||||||
|
|
||||||
|
<.list>
|
||||||
|
<:item title="Fen">{@endgame.fen}</:item>
|
||||||
|
<:item title="Key">{@endgame.key}</:item>
|
||||||
|
<:item title="Color">{@endgame.color}</:item>
|
||||||
|
<:item title="Rating">{@endgame.rating}</:item>
|
||||||
|
<:item title="Message">{@endgame.message}</:item>
|
||||||
|
<:item title="Notes">{@endgame.notes}</:item>
|
||||||
|
<:item title="Result">{@endgame.result}</:item>
|
||||||
|
<:item title="Attempted">{@endgame.times_attempted}</:item>
|
||||||
|
<:item title="Solved">{@endgame.times_solved}</:item>
|
||||||
|
</.list>
|
||||||
|
</Layouts.app>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(%{"id" => id}, _session, socket) do
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Show Endgame")
|
||||||
|
|> assign(:endgame, Endgames.get_endgame!(id))}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -22,6 +22,17 @@ defmodule ChesstrainerWeb.Router do
|
|||||||
live "/chess-test", ChessTestLive
|
live "/chess-test", ChessTestLive
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/admin", ChesstrainerWeb do
|
||||||
|
pipe_through :browser
|
||||||
|
|
||||||
|
live_session :endgames do
|
||||||
|
live "/endgames", EndgameLive.Index, :index
|
||||||
|
live "/endgames/new", EndgameLive.Form, :new
|
||||||
|
live "/endgames/:id", EndgameLive.Show, :show
|
||||||
|
live "/endgames/:id/edit", EndgameLive.Form, :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
# scope "/api", ChesstrainerWeb do
|
# scope "/api", ChesstrainerWeb do
|
||||||
# pipe_through :api
|
# pipe_through :api
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ defmodule Chesstrainer.MixProject do
|
|||||||
{:gettext, "~> 1.0"},
|
{:gettext, "~> 1.0"},
|
||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
{:dns_cluster, "~> 0.2.0"},
|
{:dns_cluster, "~> 0.2.0"},
|
||||||
{:bandit, "~> 1.5"}
|
{:bandit, "~> 1.5"},
|
||||||
|
{:chex, git: "https://github.com/therealowenrees/chex.git", tag: "v0.2.0-alpha.2"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
%{
|
%{
|
||||||
"bandit": {:hex, :bandit, "1.12.0", "6c5214daa2469644ac4ab0113b98abc24f75e348378e6a974c6343b3e5da22ef", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "45dac82dc86f45cf4a196dee9cc5a8b791d9c9469d996055f055e6ee36c66e20"},
|
"bandit": {:hex, :bandit, "1.12.0", "6c5214daa2469644ac4ab0113b98abc24f75e348378e6a974c6343b3e5da22ef", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "45dac82dc86f45cf4a196dee9cc5a8b791d9c9469d996055f055e6ee36c66e20"},
|
||||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||||
|
"chex": {:git, "https://github.com/therealowenrees/chex.git", "655b5c3df6c5297689d7872bbce1e86c6c629d26", [tag: "v0.2.0-alpha.2"]},
|
||||||
"db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"},
|
"db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"},
|
||||||
"decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
|
"decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
|
||||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||||
"mint": {:hex, :mint, "1.9.0", "d6f534c2a3e98b2a8cc749b4796eb77e9e3af79a76f96e4c74035a827de0d318", [: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", "007154c7d8c43916aed3c93afd1f11aebbaa9c5ff4b7ba55ebe0d17ee0296042"},
|
"mint": {:hex, :mint, "1.9.0", "d6f534c2a3e98b2a8cc749b4796eb77e9e3af79a76f96e4c74035a827de0d318", [: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", "007154c7d8c43916aed3c93afd1f11aebbaa9c5ff4b7ba55ebe0d17ee0296042"},
|
||||||
"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_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||||
"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.8.8", "ada3d761359274178180c0e992ef0c2b536bd7c3bd75ebba94acbf39ab4347fe", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {: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", "f0c843037bd2e7012fc1d1ec9574dfa6972b7e3d09e9b77fd23aa283af0aa994"},
|
"phoenix": {:hex, :phoenix, "1.8.8", "ada3d761359274178180c0e992ef0c2b536bd7c3bd75ebba94acbf39ab4347fe", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {: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", "f0c843037bd2e7012fc1d1ec9574dfa6972b7e3d09e9b77fd23aa283af0aa994"},
|
||||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [: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", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [: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", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
defmodule Chesstrainer.Repo.Migrations.CreateTags do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:tags) do
|
||||||
|
add :name, :string
|
||||||
|
add :category, :string
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(:tags, [:name, :category])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
defmodule Chesstrainer.Repo.Migrations.CreateEndgames do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:endgames, primary_key: false) do
|
||||||
|
add :id, :binary_id, primary_key: true
|
||||||
|
add :fen, :string
|
||||||
|
add :color, :string
|
||||||
|
add :key, :string
|
||||||
|
add :message, :text
|
||||||
|
add :notes, :text
|
||||||
|
add :result, :string
|
||||||
|
add :rating, :integer, default: 1500
|
||||||
|
add :rating_deviation, :integer, default: 350
|
||||||
|
add :times_attempted, :integer, default: 0
|
||||||
|
add :times_solved, :integer, default: 0
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(:endgames, [:fen])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
defmodule Chesstrainer.Repo.Migrations.CreateEndgamesTags do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:endgames_tags, primary_key: false) do
|
||||||
|
add :endgame_id, references(:endgames, type: :binary_id, on_delete: :delete_all)
|
||||||
|
add :tag_id, references(:tags, on_delete: :delete_all)
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(:endgames_tags, [:endgame_id, :tag_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,3 +9,57 @@
|
|||||||
#
|
#
|
||||||
# We recommend using the bang functions (`insert!`, `update!`
|
# We recommend using the bang functions (`insert!`, `update!`
|
||||||
# and so on) as they will fail if something goes wrong.
|
# and so on) as they will fail if something goes wrong.
|
||||||
|
|
||||||
|
alias Chesstrainer.Repo
|
||||||
|
alias Chesstrainer.Endgames.Endgame
|
||||||
|
alias Chesstrainer.Tags.Tag
|
||||||
|
|
||||||
|
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
||||||
|
|
||||||
|
IO.puts("🌱 Seeding basic endgames into DB...")
|
||||||
|
|
||||||
|
Repo.insert_all(Endgame, [
|
||||||
|
%{
|
||||||
|
id: Ecto.UUID.generate(),
|
||||||
|
fen: "8/8/3k4/8/8/3K1R2/8/8 w - - 0 1",
|
||||||
|
color: :white,
|
||||||
|
key: "KR v K",
|
||||||
|
message: "Rook vs king endgame",
|
||||||
|
notes: "Basic rook endgame",
|
||||||
|
result: :win,
|
||||||
|
inserted_at: now,
|
||||||
|
updated_at: now
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
id: Ecto.UUID.generate(),
|
||||||
|
fen: "6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 0",
|
||||||
|
color: :black,
|
||||||
|
key: "KPPP v KPP",
|
||||||
|
message: "3 pawns vs 2 pawns",
|
||||||
|
notes: "https://www.chessgames.com/perl/chessgame?gid=1017147",
|
||||||
|
result: :win,
|
||||||
|
inserted_at: now,
|
||||||
|
updated_at: now
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
IO.puts("✅ Done seeding endgames.")
|
||||||
|
|
||||||
|
IO.puts("🌱 Seeding endgame tags into DB...")
|
||||||
|
|
||||||
|
Repo.insert_all(Tag, [
|
||||||
|
%{
|
||||||
|
name: "mate in 2",
|
||||||
|
category: :endgame,
|
||||||
|
inserted_at: now,
|
||||||
|
updated_at: now
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "mate in 3",
|
||||||
|
category: :endgame,
|
||||||
|
inserted_at: now,
|
||||||
|
updated_at: now
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
IO.puts("✅ Done seeding endgame tags.")
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.6)">
|
||||||
|
<g style="fill:#000000; stroke:#000000; stroke-linecap:butt;">
|
||||||
|
<path d="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
|
||||||
|
<path d="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
|
||||||
|
<path d="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
|
||||||
|
</g>
|
||||||
|
<path d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" style="fill:none; stroke:#ffffff; stroke-linejoin:miter;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.6)">
|
||||||
|
<g style="fill:#000000; stroke:#000000; stroke-linecap:butt;">
|
||||||
|
<path d="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
|
||||||
|
<path d="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
|
||||||
|
<path d="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
|
||||||
|
</g>
|
||||||
|
<path d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" style="fill:none; stroke:#ffffff; stroke-linejoin:miter;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;">
|
||||||
|
<path d="M 22.5,11.63 L 22.5,6" style="fill:none; stroke:#000000; stroke-linejoin:miter;" id="path6570"/>
|
||||||
|
<path d="M 22.5,25 C 22.5,25 27,17.5 25.5,14.5 C 25.5,14.5 24.5,12 22.5,12 C 20.5,12 19.5,14.5 19.5,14.5 C 18,17.5 22.5,25 22.5,25" style="fill:#000000;fill-opacity:1; stroke-linecap:butt; stroke-linejoin:miter;"/>
|
||||||
|
<path d="M 12.5,37 C 18,40.5 27,40.5 32.5,37 L 32.5,30 C 32.5,30 41.5,25.5 38.5,19.5 C 34.5,13 25,16 22.5,23.5 L 22.5,27 L 22.5,23.5 C 20,16 10.5,13 6.5,19.5 C 3.5,25.5 12.5,30 12.5,30 L 12.5,37" style="fill:#000000; stroke:#000000;"/>
|
||||||
|
<path d="M 20,8 L 25,8" style="fill:none; stroke:#000000; stroke-linejoin:miter;"/>
|
||||||
|
<path d="M 32,29.5 C 32,29.5 40.5,25.5 38.03,19.85 C 34.15,14 25,18 22.5,24.5 L 22.5,26.6 L 22.5,24.5 C 20,18 10.85,14 6.97,19.85 C 4.5,25.5 13,29.5 13,29.5" style="fill:none; stroke:#ffffff;"/>
|
||||||
|
<path d="M 12.5,30 C 18,27 27,27 32.5,30 M 12.5,33.5 C 18,30.5 27,30.5 32.5,33.5 M 12.5,37 C 18,34 27,34 32.5,37" style="fill:none; stroke:#ffffff;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;">
|
||||||
|
<path d="M 22.5,11.63 L 22.5,6" style="fill:none; stroke:#000000; stroke-linejoin:miter;" id="path6570"/>
|
||||||
|
<path d="M 22.5,25 C 22.5,25 27,17.5 25.5,14.5 C 25.5,14.5 24.5,12 22.5,12 C 20.5,12 19.5,14.5 19.5,14.5 C 18,17.5 22.5,25 22.5,25" style="fill:#000000;fill-opacity:1; stroke-linecap:butt; stroke-linejoin:miter;"/>
|
||||||
|
<path d="M 12.5,37 C 18,40.5 27,40.5 32.5,37 L 32.5,30 C 32.5,30 41.5,25.5 38.5,19.5 C 34.5,13 25,16 22.5,23.5 L 22.5,27 L 22.5,23.5 C 20,16 10.5,13 6.5,19.5 C 3.5,25.5 12.5,30 12.5,30 L 12.5,37" style="fill:#000000; stroke:#000000;"/>
|
||||||
|
<path d="M 20,8 L 25,8" style="fill:none; stroke:#000000; stroke-linejoin:miter;"/>
|
||||||
|
<path d="M 32,29.5 C 32,29.5 40.5,25.5 38.03,19.85 C 34.15,14 25,18 22.5,24.5 L 22.5,26.6 L 22.5,24.5 C 20,18 10.85,14 6.97,19.85 C 4.5,25.5 13,29.5 13,29.5" style="fill:none; stroke:#ffffff;"/>
|
||||||
|
<path d="M 12.5,30 C 18,27 27,27 32.5,30 M 12.5,33.5 C 18,30.5 27,30.5 32.5,33.5 M 12.5,37 C 18,34 27,34 32.5,37" style="fill:none; stroke:#ffffff;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||||
|
<path
|
||||||
|
d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"
|
||||||
|
style="fill:#000000; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"
|
||||||
|
style="fill:#000000; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"
|
||||||
|
style="fill:#ffffff; stroke:#ffffff;" />
|
||||||
|
<path
|
||||||
|
d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z"
|
||||||
|
transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)"
|
||||||
|
style="fill:#ffffff; stroke:#ffffff;" />
|
||||||
|
<path
|
||||||
|
d="M 24.55,10.4 L 24.1,11.85 L 24.6,12 C 27.75,13 30.25,14.49 32.5,18.75 C 34.75,23.01 35.75,29.06 35.25,39 L 35.2,39.5 L 37.45,39.5 L 37.5,39 C 38,28.94 36.62,22.15 34.25,17.66 C 31.88,13.17 28.46,11.02 25.06,10.5 L 24.55,10.4 z "
|
||||||
|
style="fill:#ffffff; stroke:none;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||||
|
<path
|
||||||
|
d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"
|
||||||
|
style="fill:#000000; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"
|
||||||
|
style="fill:#000000; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"
|
||||||
|
style="fill:#ffffff; stroke:#ffffff;" />
|
||||||
|
<path
|
||||||
|
d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z"
|
||||||
|
transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)"
|
||||||
|
style="fill:#ffffff; stroke:#ffffff;" />
|
||||||
|
<path
|
||||||
|
d="M 24.55,10.4 L 24.1,11.85 L 24.6,12 C 27.75,13 30.25,14.49 32.5,18.75 C 34.75,23.01 35.75,29.06 35.25,39 L 35.2,39.5 L 37.45,39.5 L 37.5,39 C 38,28.94 36.62,22.15 34.25,17.66 C 31.88,13.17 28.46,11.02 25.06,10.5 L 24.55,10.4 z "
|
||||||
|
style="fill:#ffffff; stroke:none;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<path d="m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z" style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 766 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<path d="m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z" style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 766 B |
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45"
|
||||||
|
height="45">
|
||||||
|
<g style="fill:#000000;stroke:#000000;stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round">
|
||||||
|
|
||||||
|
<path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"
|
||||||
|
style="stroke-linecap:butt;fill:#000000" />
|
||||||
|
<path d="m 9,26 c 0,2 1.5,2 2.5,4 1,1.5 1,1 0.5,3.5 -1.5,1 -1,2.5 -1,2.5 -1.5,1.5 0,2.5 0,2.5 6.5,1 16.5,1 23,0 0,0 1.5,-1 0,-2.5 0,0 0.5,-1.5 -1,-2.5 -0.5,-2.5 -0.5,-2 0.5,-3.5 1,-2 2.5,-2 2.5,-4 -8.5,-1.5 -18.5,-1.5 -27,0 z" />
|
||||||
|
<path d="M 11.5,30 C 15,29 30,29 33.5,30" />
|
||||||
|
<path d="m 12,33.5 c 6,-1 15,-1 21,0" />
|
||||||
|
<circle cx="6" cy="12" r="2" />
|
||||||
|
<circle cx="14" cy="9" r="2" />
|
||||||
|
<circle cx="22.5" cy="8" r="2" />
|
||||||
|
<circle cx="31" cy="9" r="2" />
|
||||||
|
<circle cx="39" cy="12" r="2" />
|
||||||
|
<path d="M 11,38.5 A 35,35 1 0 0 34,38.5"
|
||||||
|
style="fill:none; stroke:#000000;stroke-linecap:butt;" />
|
||||||
|
<g style="fill:none; stroke:#ffffff;">
|
||||||
|
<path d="M 11,29 A 35,35 1 0 1 34,29" />
|
||||||
|
<path d="M 12.5,31.5 L 32.5,31.5" />
|
||||||
|
<path d="M 11.5,34.5 A 35,35 1 0 0 33.5,34.5" />
|
||||||
|
<path d="M 10.5,37.5 A 35,35 1 0 0 34.5,37.5" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45"
|
||||||
|
height="45">
|
||||||
|
<g style="fill:#000000;stroke:#000000;stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round">
|
||||||
|
|
||||||
|
<path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"
|
||||||
|
style="stroke-linecap:butt;fill:#000000" />
|
||||||
|
<path d="m 9,26 c 0,2 1.5,2 2.5,4 1,1.5 1,1 0.5,3.5 -1.5,1 -1,2.5 -1,2.5 -1.5,1.5 0,2.5 0,2.5 6.5,1 16.5,1 23,0 0,0 1.5,-1 0,-2.5 0,0 0.5,-1.5 -1,-2.5 -0.5,-2.5 -0.5,-2 0.5,-3.5 1,-2 2.5,-2 2.5,-4 -8.5,-1.5 -18.5,-1.5 -27,0 z" />
|
||||||
|
<path d="M 11.5,30 C 15,29 30,29 33.5,30" />
|
||||||
|
<path d="m 12,33.5 c 6,-1 15,-1 21,0" />
|
||||||
|
<circle cx="6" cy="12" r="2" />
|
||||||
|
<circle cx="14" cy="9" r="2" />
|
||||||
|
<circle cx="22.5" cy="8" r="2" />
|
||||||
|
<circle cx="31" cy="9" r="2" />
|
||||||
|
<circle cx="39" cy="12" r="2" />
|
||||||
|
<path d="M 11,38.5 A 35,35 1 0 0 34,38.5"
|
||||||
|
style="fill:none; stroke:#000000;stroke-linecap:butt;" />
|
||||||
|
<g style="fill:none; stroke:#ffffff;">
|
||||||
|
<path d="M 11,29 A 35,35 1 0 1 34,29" />
|
||||||
|
<path d="M 12.5,31.5 L 32.5,31.5" />
|
||||||
|
<path d="M 11.5,34.5 A 35,35 1 0 0 33.5,34.5" />
|
||||||
|
<path d="M 10.5,37.5 A 35,35 1 0 0 34.5,37.5" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||||
|
<path
|
||||||
|
d="M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 12.5,32 L 14,29.5 L 31,29.5 L 32.5,32 L 12.5,32 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 14,29.5 L 14,16.5 L 31,16.5 L 31,29.5 L 14,29.5 z "
|
||||||
|
style="stroke-linecap:butt;stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 14,16.5 L 11,14 L 34,14 L 31,16.5 L 14,16.5 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14 L 11,14 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 12,35.5 L 33,35.5 L 33,35.5"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 13,31.5 L 32,31.5"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 14,29.5 L 31,29.5"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 14,16.5 L 31,16.5"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 11,14 L 34,14"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||||
|
<path
|
||||||
|
d="M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 12.5,32 L 14,29.5 L 31,29.5 L 32.5,32 L 12.5,32 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 14,29.5 L 14,16.5 L 31,16.5 L 31,29.5 L 14,29.5 z "
|
||||||
|
style="stroke-linecap:butt;stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 14,16.5 L 11,14 L 34,14 L 31,16.5 L 14,16.5 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14 L 11,14 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 12,35.5 L 33,35.5 L 33,35.5"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 13,31.5 L 32,31.5"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 14,29.5 L 31,29.5"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 14,16.5 L 31,16.5"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 11,14 L 34,14"
|
||||||
|
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.6)">
|
||||||
|
<g style="fill:#ffffff; stroke:#000000; stroke-linecap:butt;">
|
||||||
|
<path d="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
|
||||||
|
<path d="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
|
||||||
|
<path d="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
|
||||||
|
</g>
|
||||||
|
<path d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" style="fill:none; stroke:#000000; stroke-linejoin:miter;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.6)">
|
||||||
|
<g style="fill:#ffffff; stroke:#000000; stroke-linecap:butt;">
|
||||||
|
<path d="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
|
||||||
|
<path d="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
|
||||||
|
<path d="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
|
||||||
|
</g>
|
||||||
|
<path d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" style="fill:none; stroke:#000000; stroke-linejoin:miter;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45">
|
||||||
|
<g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||||
|
<path stroke-linejoin="miter" d="M22.5 11.63V6M20 8h5"/>
|
||||||
|
<path fill="#fff" stroke-linecap="butt" stroke-linejoin="miter" d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5"/>
|
||||||
|
<path fill="#fff" d="M12.5 37c5.5 3.5 14.5 3.5 20 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-2.5-7.5-12-10.5-16-4-3 6 6 10.5 6 10.5v7"/>
|
||||||
|
<path d="M12.5 30c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 700 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45">
|
||||||
|
<g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||||
|
<path stroke-linejoin="miter" d="M22.5 11.63V6M20 8h5"/>
|
||||||
|
<path fill="#fff" stroke-linecap="butt" stroke-linejoin="miter" d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5"/>
|
||||||
|
<path fill="#fff" d="M12.5 37c5.5 3.5 14.5 3.5 20 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-2.5-7.5-12-10.5-16-4-3 6 6 10.5 6 10.5v7"/>
|
||||||
|
<path d="M12.5 30c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 700 B |
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||||
|
<path
|
||||||
|
d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"
|
||||||
|
style="fill:#ffffff; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"
|
||||||
|
style="fill:#ffffff; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"
|
||||||
|
style="fill:#000000; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z"
|
||||||
|
transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)"
|
||||||
|
style="fill:#000000; stroke:#000000;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||||
|
<path
|
||||||
|
d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"
|
||||||
|
style="fill:#ffffff; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"
|
||||||
|
style="fill:#ffffff; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"
|
||||||
|
style="fill:#000000; stroke:#000000;" />
|
||||||
|
<path
|
||||||
|
d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z"
|
||||||
|
transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)"
|
||||||
|
style="fill:#000000; stroke:#000000;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<path d="m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z" style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 766 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<path d="m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z" style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 766 B |
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="fill:#ffffff;stroke:#000000;stroke-width:1.5;stroke-linejoin:round">
|
||||||
|
<path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"/>
|
||||||
|
<path d="M 9,26 C 9,28 10.5,28 11.5,30 C 12.5,31.5 12.5,31 12,33.5 C 10.5,34.5 11,36 11,36 C 9.5,37.5 11,38.5 11,38.5 C 17.5,39.5 27.5,39.5 34,38.5 C 34,38.5 35.5,37.5 34,36 C 34,36 34.5,34.5 33,33.5 C 32.5,31 32.5,31.5 33.5,30 C 34.5,28 36,28 36,26 C 27.5,24.5 17.5,24.5 9,26 z"/>
|
||||||
|
<path d="M 11.5,30 C 15,29 30,29 33.5,30" style="fill:none"/>
|
||||||
|
<path d="M 12,33.5 C 18,32.5 27,32.5 33,33.5" style="fill:none"/>
|
||||||
|
<circle cx="6" cy="12" r="2" />
|
||||||
|
<circle cx="14" cy="9" r="2" />
|
||||||
|
<circle cx="22.5" cy="8" r="2" />
|
||||||
|
<circle cx="31" cy="9" r="2" />
|
||||||
|
<circle cx="39" cy="12" r="2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="fill:#ffffff;stroke:#000000;stroke-width:1.5;stroke-linejoin:round">
|
||||||
|
<path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"/>
|
||||||
|
<path d="M 9,26 C 9,28 10.5,28 11.5,30 C 12.5,31.5 12.5,31 12,33.5 C 10.5,34.5 11,36 11,36 C 9.5,37.5 11,38.5 11,38.5 C 17.5,39.5 27.5,39.5 34,38.5 C 34,38.5 35.5,37.5 34,36 C 34,36 34.5,34.5 33,33.5 C 32.5,31 32.5,31.5 33.5,30 C 34.5,28 36,28 36,26 C 27.5,24.5 17.5,24.5 9,26 z"/>
|
||||||
|
<path d="M 11.5,30 C 15,29 30,29 33.5,30" style="fill:none"/>
|
||||||
|
<path d="M 12,33.5 C 18,32.5 27,32.5 33,33.5" style="fill:none"/>
|
||||||
|
<circle cx="6" cy="12" r="2" />
|
||||||
|
<circle cx="14" cy="9" r="2" />
|
||||||
|
<circle cx="22.5" cy="8" r="2" />
|
||||||
|
<circle cx="31" cy="9" r="2" />
|
||||||
|
<circle cx="39" cy="12" r="2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||||
|
<path
|
||||||
|
d="M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14"
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 34,14 L 31,17 L 14,17 L 11,14" />
|
||||||
|
<path
|
||||||
|
d="M 31,17 L 31,29.5 L 14,29.5 L 14,17"
|
||||||
|
style="stroke-linecap:butt; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 31,29.5 L 32.5,32 L 12.5,32 L 14,29.5" />
|
||||||
|
<path
|
||||||
|
d="M 11,14 L 34,14"
|
||||||
|
style="fill:none; stroke:#000000; stroke-linejoin:miter;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||||
|
<g style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||||
|
<path
|
||||||
|
d="M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z "
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14"
|
||||||
|
style="stroke-linecap:butt;" />
|
||||||
|
<path
|
||||||
|
d="M 34,14 L 31,17 L 14,17 L 11,14" />
|
||||||
|
<path
|
||||||
|
d="M 31,17 L 31,29.5 L 14,29.5 L 14,17"
|
||||||
|
style="stroke-linecap:butt; stroke-linejoin:miter;" />
|
||||||
|
<path
|
||||||
|
d="M 31,29.5 L 32.5,32 L 12.5,32 L 14,29.5" />
|
||||||
|
<path
|
||||||
|
d="M 11,14 L 34,14"
|
||||||
|
style="fill:none; stroke:#000000; stroke-linejoin:miter;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,61 @@
|
|||||||
|
defmodule Chesstrainer.TagsTest do
|
||||||
|
use Chesstrainer.DataCase
|
||||||
|
|
||||||
|
alias Chesstrainer.Tags
|
||||||
|
|
||||||
|
describe "tags" do
|
||||||
|
alias Chesstrainer.Tags.Tag
|
||||||
|
|
||||||
|
import Chesstrainer.TagsFixtures
|
||||||
|
|
||||||
|
@invalid_attrs %{name: nil, category: nil}
|
||||||
|
|
||||||
|
test "list_tags/0 returns all tags" do
|
||||||
|
tag = tag_fixture()
|
||||||
|
assert Tags.list_tags() == [tag]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "get_tag!/1 returns the tag with given id" do
|
||||||
|
tag = tag_fixture()
|
||||||
|
assert Tags.get_tag!(tag.id) == tag
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create_tag/1 with valid data creates a tag" do
|
||||||
|
valid_attrs = %{name: "some name", category: "some category"}
|
||||||
|
|
||||||
|
assert {:ok, %Tag{} = tag} = Tags.create_tag(valid_attrs)
|
||||||
|
assert tag.name == "some name"
|
||||||
|
assert tag.category == "some category"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create_tag/1 with invalid data returns error changeset" do
|
||||||
|
assert {:error, %Ecto.Changeset{}} = Tags.create_tag(@invalid_attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "update_tag/2 with valid data updates the tag" do
|
||||||
|
tag = tag_fixture()
|
||||||
|
update_attrs = %{name: "some updated name", category: "some updated category"}
|
||||||
|
|
||||||
|
assert {:ok, %Tag{} = tag} = Tags.update_tag(tag, update_attrs)
|
||||||
|
assert tag.name == "some updated name"
|
||||||
|
assert tag.category == "some updated category"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "update_tag/2 with invalid data returns error changeset" do
|
||||||
|
tag = tag_fixture()
|
||||||
|
assert {:error, %Ecto.Changeset{}} = Tags.update_tag(tag, @invalid_attrs)
|
||||||
|
assert tag == Tags.get_tag!(tag.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "delete_tag/1 deletes the tag" do
|
||||||
|
tag = tag_fixture()
|
||||||
|
assert {:ok, %Tag{}} = Tags.delete_tag(tag)
|
||||||
|
assert_raise Ecto.NoResultsError, fn -> Tags.get_tag!(tag.id) end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "change_tag/1 returns a tag changeset" do
|
||||||
|
tag = tag_fixture()
|
||||||
|
assert %Ecto.Changeset{} = Tags.change_tag(tag)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,6 +9,7 @@ defmodule ChesstrainerWeb.ErrorHTMLTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "renders 500.html" do
|
test "renders 500.html" do
|
||||||
assert render_to_string(ChesstrainerWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
|
assert render_to_string(ChesstrainerWeb.ErrorHTML, "500", "html", []) ==
|
||||||
|
"Internal Server Error"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
defmodule Chesstrainer.TagsFixtures do
|
||||||
|
@moduledoc """
|
||||||
|
This module defines test helpers for creating
|
||||||
|
entities via the `Chesstrainer.Tags` context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate a unique tag name.
|
||||||
|
"""
|
||||||
|
def unique_tag_name, do: "some name#{System.unique_integer([:positive])}"
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate a tag.
|
||||||
|
"""
|
||||||
|
def tag_fixture(attrs \\ %{}) do
|
||||||
|
{:ok, tag} =
|
||||||
|
attrs
|
||||||
|
|> Enum.into(%{
|
||||||
|
category: "some category",
|
||||||
|
name: unique_tag_name()
|
||||||
|
})
|
||||||
|
|> Chesstrainer.Tags.create_tag()
|
||||||
|
|
||||||
|
tag
|
||||||
|
end
|
||||||
|
end
|
||||||