forked from skji/kink-bio
wip: messing with some stateless stuff #1
54 changed files with 1584 additions and 1192 deletions
|
|
@ -1,5 +1,6 @@
|
|||
[
|
||||
import_deps: [:ecto, :phoenix],
|
||||
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
subdirectories: ["priv/*/migrations"]
|
||||
import_deps: [:ecto, :ecto_sql, :phoenix],
|
||||
subdirectories: ["priv/*/migrations"],
|
||||
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
|
||||
]
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -24,6 +24,9 @@ kink_bio-*.tar
|
|||
|
||||
# Ignore assets that are produced by build tools.
|
||||
/priv/static/assets/
|
||||
/priv/static/favicon-*
|
||||
/priv/static/robots-*
|
||||
/priv/static/robots.txt.*
|
||||
|
||||
# Ignore digested assets cache.
|
||||
/priv/static/cache_manifest.json
|
||||
|
|
|
|||
|
|
@ -1,197 +1,178 @@
|
|||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
@import url("https://unpkg.com/@catppuccin/palette/css/catppuccin.css");
|
||||
@import url("https://use.typekit.net/gzb5jcn.css");
|
||||
@import "tailwindcss" source(none);
|
||||
@import "@catppuccin/tailwindcss/mocha.css";
|
||||
|
||||
@source "../css";
|
||||
@source "../js";
|
||||
@source "../../lib/kink_bio_web";
|
||||
|
||||
@plugin "@tailwindcss/forms";
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes: false;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "dark";
|
||||
default: false;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--color-base-100: oklch(30.33% 0.016 252.42);
|
||||
--color-base-200: oklch(25.26% 0.014 253.1);
|
||||
--color-base-300: oklch(20.15% 0.012 254.09);
|
||||
--color-base-content: oklch(97.807% 0.029 256.847);
|
||||
--color-primary: oklch(58% 0.233 277.117);
|
||||
--color-primary-content: oklch(96% 0.018 272.314);
|
||||
--color-secondary: oklch(58% 0.233 277.117);
|
||||
--color-secondary-content: oklch(96% 0.018 272.314);
|
||||
--color-accent: oklch(60% 0.25 292.717);
|
||||
--color-accent-content: oklch(96% 0.016 293.756);
|
||||
--color-neutral: oklch(37% 0.044 257.287);
|
||||
--color-neutral-content: oklch(98% 0.003 247.858);
|
||||
--color-info: oklch(58% 0.158 241.966);
|
||||
--color-info-content: oklch(97% 0.013 236.62);
|
||||
--color-success: oklch(60% 0.118 184.704);
|
||||
--color-success-content: oklch(98% 0.014 180.72);
|
||||
--color-warning: oklch(66% 0.179 58.318);
|
||||
--color-warning-content: oklch(98% 0.022 95.277);
|
||||
--color-error: oklch(58% 0.253 17.585);
|
||||
--color-error-content: oklch(96% 0.015 12.422);
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.25rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.21875rem;
|
||||
--size-field: 0.21875rem;
|
||||
--border: 1.5px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
default: true;
|
||||
prefersdark: false;
|
||||
color-scheme: "light";
|
||||
--color-base-100: oklch(98% 0 0);
|
||||
--color-base-200: oklch(96% 0.001 286.375);
|
||||
--color-base-300: oklch(92% 0.004 286.32);
|
||||
--color-base-content: oklch(21% 0.006 285.885);
|
||||
--color-primary: oklch(70% 0.213 47.604);
|
||||
--color-primary-content: oklch(98% 0.016 73.684);
|
||||
--color-secondary: oklch(55% 0.027 264.364);
|
||||
--color-secondary-content: oklch(98% 0.002 247.839);
|
||||
--color-accent: oklch(0% 0 0);
|
||||
--color-accent-content: oklch(100% 0 0);
|
||||
--color-neutral: oklch(44% 0.017 285.786);
|
||||
--color-neutral-content: oklch(98% 0 0);
|
||||
--color-info: oklch(62% 0.214 259.815);
|
||||
--color-info-content: oklch(97% 0.014 254.604);
|
||||
--color-success: oklch(70% 0.14 182.503);
|
||||
--color-success-content: oklch(98% 0.014 180.72);
|
||||
--color-warning: oklch(66% 0.179 58.318);
|
||||
--color-warning-content: oklch(98% 0.022 95.277);
|
||||
--color-error: oklch(58% 0.253 17.585);
|
||||
--color-error-content: oklch(96% 0.015 12.422);
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.25rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.21875rem;
|
||||
--size-field: 0.21875rem;
|
||||
--border: 1.5px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Add variants based on LiveView classes */
|
||||
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
|
||||
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
|
||||
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
|
||||
|
||||
/* Use the data attribute for dark mode */
|
||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||
|
||||
/* Make LiveView wrapper divs transparent for layout */
|
||||
[data-phx-session], [data-phx-teleported-src] { display: contents }
|
||||
|
||||
/* This file is for your main application CSS */
|
||||
|
||||
@theme {
|
||||
--font-body: "basic-sans", sans-serif;
|
||||
--font-display: "zeitung", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--fa-primary-color: var(--ctp-mocha-pink);
|
||||
--fa-secondary-color: var(--ctp-mocha-mauve);
|
||||
--fa-primary-color: var(--color-ctp-pink);
|
||||
--fa-secondary-color: var(--color-ctp-mauve);
|
||||
}
|
||||
|
||||
/* This file is for your main application CSS */
|
||||
|
||||
@layer utilities {
|
||||
.tooltip {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip::before {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: none;
|
||||
background-color: var(--ctp-mocha-crust);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tooltip::before {
|
||||
min-width: calc(31.5rem - 1.75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.tooltip::before {
|
||||
min-width: calc(100vw - 1.75rem);
|
||||
}
|
||||
.tooltip {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip::before {
|
||||
top: 100%;
|
||||
transform: translateX(-1.25rem);
|
||||
margin: 0.25rem 0.75rem 0;
|
||||
}
|
||||
|
||||
.tooltip:hover::before,
|
||||
.tooltip:active::before {
|
||||
display: block;
|
||||
}
|
||||
@utility bg-pos-0 {
|
||||
background-position: "0% 0%";
|
||||
}
|
||||
|
||||
/* Alerts and form errors used by phx.new */
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
@utility bg-pos-100 {
|
||||
background-position: "100% 100%";
|
||||
}
|
||||
.alert-info {
|
||||
color: #31708f;
|
||||
background-color: #d9edf7;
|
||||
border-color: #bce8f1;
|
||||
|
||||
@utility bg-size-200 {
|
||||
background-size: "200% 200%";
|
||||
}
|
||||
.alert-warning {
|
||||
color: #8a6d3b;
|
||||
background-color: #fcf8e3;
|
||||
border-color: #faebcc;
|
||||
|
||||
@utility bg-size-300 {
|
||||
background-size: "300% 300%";
|
||||
}
|
||||
.alert-danger {
|
||||
color: #a94442;
|
||||
background-color: #f2dede;
|
||||
border-color: #ebccd1;
|
||||
}
|
||||
.alert p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.alert:empty {
|
||||
|
||||
@utility tooltip {
|
||||
position: relative;
|
||||
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
|
||||
&::before {
|
||||
content: attr(data-text);
|
||||
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 100%;
|
||||
transform: translateX(-1.25rem);
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
margin: 0.25rem 0.75rem 0;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: none;
|
||||
}
|
||||
.invalid-feedback {
|
||||
color: #a94442;
|
||||
background-color: var(--color-ctp-crust);
|
||||
}
|
||||
|
||||
&:hover::before,
|
||||
&:active::before {
|
||||
display: block;
|
||||
margin: -1rem 0 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* LiveView specific classes for your customization */
|
||||
.phx-no-feedback.invalid-feedback,
|
||||
.phx-no-feedback .invalid-feedback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.phx-click-loading {
|
||||
opacity: 0.5;
|
||||
transition: opacity 1s ease-out;
|
||||
}
|
||||
|
||||
.phx-loading {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.phx-modal {
|
||||
opacity: 1 !important;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.phx-modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 15vh auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.phx-modal-close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.phx-modal-close:hover,
|
||||
.phx-modal-close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fade-in-scale {
|
||||
animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys;
|
||||
}
|
||||
|
||||
.fade-out-scale {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
||||
}
|
||||
.fade-out {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
|
||||
}
|
||||
|
||||
@keyframes fade-in-scale-keys {
|
||||
0% {
|
||||
scale: 0.95;
|
||||
opacity: 0;
|
||||
@media (width >= 48rem) {
|
||||
&::before {
|
||||
min-width: calc(31.5rem - 1.75rem);
|
||||
}
|
||||
100% {
|
||||
scale: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (width < 48rem) {
|
||||
&::before {
|
||||
overflow-x: hidden;
|
||||
min-width: calc(100vw - 2.75rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out-scale-keys {
|
||||
0% {
|
||||
scale: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
scale: 0.95;
|
||||
opacity: 0;
|
||||
}
|
||||
@utility fa-primary-* {
|
||||
--fa-primary-color: --value(--color-*, [color]);
|
||||
--fa-primary-opacity: --value(integer, [integer])%;
|
||||
}
|
||||
|
||||
@keyframes fade-in-keys {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out-keys {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
@utility fa-secondary-* {
|
||||
--fa-secondary-color: --value(--color-*, [color]);
|
||||
--fa-secondary-opacity: --value(integer, [integer])%;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,3 @@
|
|||
// We import the CSS which is extracted to its own file by esbuild.
|
||||
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
|
||||
|
||||
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
|
||||
// to get started and then uncomment the line below.
|
||||
// import "./user_socket.js"
|
||||
|
|
@ -17,21 +14,28 @@
|
|||
//
|
||||
// import "some-package"
|
||||
//
|
||||
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` 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.
|
||||
import "phoenix_html"
|
||||
// Establish Phoenix Socket and LiveView configuration.
|
||||
import { Socket } from "phoenix"
|
||||
import { LiveSocket } from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
import { hooks as colocatedHooks } from "phoenix-colocated/kink_bio"
|
||||
import topbar from "topbar"
|
||||
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } })
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
const liveSocket = new LiveSocket("/live", Socket, {
|
||||
longPollFallbackMs: 2500,
|
||||
params: { _csrf_token: csrfToken },
|
||||
hooks: { ...colocatedHooks },
|
||||
})
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
|
||||
window.addEventListener("phx:page-loading-start", info => topbar.show())
|
||||
window.addEventListener("phx:page-loading-stop", info => topbar.hide())
|
||||
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
|
||||
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
|
||||
|
||||
// connect if there are any LiveViews on the page
|
||||
liveSocket.connect()
|
||||
|
|
@ -42,3 +46,37 @@ liveSocket.connect()
|
|||
// >> liveSocket.disableLatencySim()
|
||||
window.liveSocket = liveSocket
|
||||
|
||||
// The lines below enable quality of life phoenix_live_reload
|
||||
// development features:
|
||||
//
|
||||
// 1. stream server logs to the browser console
|
||||
// 2. click on elements to jump to their definitions in your code editor
|
||||
//
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
window.addEventListener("phx:live_reload:attached", ({ detail: reloader }) => {
|
||||
// Enable server log streaming to client.
|
||||
// Disable with reloader.disableServerLogs()
|
||||
reloader.enableServerLogs()
|
||||
|
||||
// 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 "d" key pressed to open at function component definition location
|
||||
let keyDown
|
||||
window.addEventListener("keydown", e => keyDown = e.key)
|
||||
window.addEventListener("keyup", _e => keyDown = null)
|
||||
window.addEventListener("click", e => {
|
||||
if (keyDown === "c") {
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
reloader.openEditorAtCaller(e.target)
|
||||
} else if (keyDown === "d") {
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
reloader.openEditorAtDef(e.target)
|
||||
}
|
||||
}, true)
|
||||
|
||||
window.liveReloader = reloader
|
||||
})
|
||||
}
|
||||
|
|
|
|||
67
assets/package-lock.json
generated
Normal file
67
assets/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"name": "assets",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@catppuccin/tailwindcss": "^1.0.0",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"daisyui": "^5.5.14",
|
||||
"topbar": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@catppuccin/tailwindcss": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@catppuccin/tailwindcss/-/tailwindcss-1.0.0.tgz",
|
||||
"integrity": "sha512-l8pOlcYe2ncGd8a1gUmL5AHmKlxR2+CHuG5kt4Me6IZwzntW1DoLmj89BH+DcsPHBsdDGLrTSv35emlYyU3FeQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz",
|
||||
"integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mini-svg-data-uri": "^1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.5.14",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.14.tgz",
|
||||
"integrity": "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mini-svg-data-uri": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/topbar": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/topbar/-/topbar-3.0.0.tgz",
|
||||
"integrity": "sha512-mhczD7KfYi1anfoMPKRdl0wPSWiYc0YOK4KyycYs3EaNT15pVVNDG5CtfgZcEBWIPJEdfR7r8K4hTXDD2ECBVQ==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
assets/package.json
Normal file
8
assets/package.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@catppuccin/tailwindcss": "^1.0.0",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"daisyui": "^5.5.14",
|
||||
"topbar": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
// See the Tailwind configuration guide for advanced usage
|
||||
// https://tailwindcss.com/docs/configuration
|
||||
|
||||
let plugin = require("tailwindcss/plugin");
|
||||
|
||||
const ctp = ([str]) => `rgb(var(--ctp-mocha-${str}-rgb) / <alpha-value>)`;
|
||||
|
||||
const ctpColors = {
|
||||
rosewater: ctp`rosewater`,
|
||||
flamingo: ctp`flamingo`,
|
||||
pink: ctp`pink`,
|
||||
mauve: ctp`mauve`,
|
||||
red: ctp`red`,
|
||||
maroon: ctp`maroon`,
|
||||
peach: ctp`peach`,
|
||||
yellow: ctp`yellow`,
|
||||
green: ctp`green`,
|
||||
teal: ctp`teal`,
|
||||
sky: ctp`sky`,
|
||||
sapphire: ctp`sapphire`,
|
||||
blue: ctp`blue`,
|
||||
lavender: ctp`lavender`,
|
||||
/* ui styles */
|
||||
text: ctp`text`,
|
||||
subtext1: ctp`subtext1`,
|
||||
subtext0: ctp`subtext0`,
|
||||
overlay2: ctp`overlay2`,
|
||||
overlay1: ctp`overlay1`,
|
||||
overlay0: ctp`overlay0`,
|
||||
surface2: ctp`surface2`,
|
||||
surface1: ctp`surface1`,
|
||||
surface0: ctp`surface0`,
|
||||
base: ctp`base`,
|
||||
mantle: ctp`mantle`,
|
||||
crust: ctp`crust`,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
content: ["./js/**/*.js", "../lib/*_web.ex", "../lib/*_web/**/*.*ex"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
...ctpColors,
|
||||
},
|
||||
backgroundSize: {
|
||||
"size-200": "200% 200%",
|
||||
"size-300": "300% 300%",
|
||||
},
|
||||
backgroundPosition: {
|
||||
"pos-0": "0% 0%",
|
||||
"pos-100": "100% 100%",
|
||||
},
|
||||
fontFamily: {
|
||||
display: ["zeitung", "sans-serif"],
|
||||
body: ["basic-sans", "sans-serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("@tailwindcss/forms"),
|
||||
plugin(({ addVariant }) =>
|
||||
addVariant("phx-no-feedback", [
|
||||
"&.phx-no-feedback",
|
||||
".phx-no-feedback &",
|
||||
]),
|
||||
),
|
||||
plugin(({ addVariant }) =>
|
||||
addVariant("phx-click-loading", [
|
||||
"&.phx-click-loading",
|
||||
".phx-click-loading &",
|
||||
]),
|
||||
),
|
||||
plugin(({ addVariant }) =>
|
||||
addVariant("phx-submit-loading", [
|
||||
"&.phx-submit-loading",
|
||||
".phx-submit-loading &",
|
||||
]),
|
||||
),
|
||||
plugin(({ addVariant }) =>
|
||||
addVariant("phx-change-loading", [
|
||||
"&.phx-change-loading",
|
||||
".phx-change-loading &",
|
||||
]),
|
||||
),
|
||||
plugin(function ({ matchUtilities, theme }) {
|
||||
matchUtilities(
|
||||
{
|
||||
"fa-primary": (fn, x) => ({
|
||||
"--fa-primary-color": fn(x),
|
||||
}),
|
||||
"fa-secondary": (fn, x) => ({
|
||||
"--fa-secondary-color": fn(x),
|
||||
}),
|
||||
fa: (fn, x) => ({
|
||||
"--fa-primary-color": fn(x),
|
||||
"--fa-secondary-color": fn(x),
|
||||
}),
|
||||
},
|
||||
{ type: ["color"], values: ctpColors },
|
||||
);
|
||||
}),
|
||||
plugin(function ({ matchUtilities, theme }) {
|
||||
matchUtilities(
|
||||
{
|
||||
"fa-primary": (x) => ({
|
||||
"--fa-primary-opacity": x,
|
||||
}),
|
||||
"fa-secondary": (x) => ({
|
||||
"--fa-secondary-opacity": x,
|
||||
}),
|
||||
},
|
||||
{ values: theme("opacity") },
|
||||
);
|
||||
}),
|
||||
],
|
||||
};
|
||||
32
assets/tsconfig.json
Normal file
32
assets/tsconfig.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// This file is needed on most editors to enable the intelligent autocompletion
|
||||
// of LiveView's JavaScript API methods. You can safely delete it if you don't need it.
|
||||
//
|
||||
// Note: This file assumes a basic esbuild setup without node_modules.
|
||||
// We include a generic paths alias to deps to mimic how esbuild resolves
|
||||
// the Phoenix and LiveView JavaScript assets.
|
||||
// If you have a package.json in your project, you should remove the
|
||||
// paths configuration and instead add the phoenix dependencies to the
|
||||
// dependencies section of your package.json:
|
||||
//
|
||||
// {
|
||||
// ...
|
||||
// "dependencies": {
|
||||
// ...,
|
||||
// "phoenix": "../deps/phoenix",
|
||||
// "phoenix_html": "../deps/phoenix_html",
|
||||
// "phoenix_live_view": "../deps/phoenix_live_view"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Feel free to adjust this configuration however you need.
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["../deps/*"]
|
||||
},
|
||||
"allowJs": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["js/**/*"]
|
||||
}
|
||||
157
assets/vendor/topbar.js
vendored
157
assets/vendor/topbar.js
vendored
|
|
@ -1,157 +0,0 @@
|
|||
/**
|
||||
* @license MIT
|
||||
* topbar 1.0.0, 2021-01-06
|
||||
* https://buunguyen.github.io/topbar
|
||||
* Copyright (c) 2021 Buu Nguyen
|
||||
*/
|
||||
(function (window, document) {
|
||||
"use strict";
|
||||
|
||||
// https://gist.github.com/paulirish/1579671
|
||||
(function () {
|
||||
var lastTime = 0;
|
||||
var vendors = ["ms", "moz", "webkit", "o"];
|
||||
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
|
||||
window.requestAnimationFrame =
|
||||
window[vendors[x] + "RequestAnimationFrame"];
|
||||
window.cancelAnimationFrame =
|
||||
window[vendors[x] + "CancelAnimationFrame"] ||
|
||||
window[vendors[x] + "CancelRequestAnimationFrame"];
|
||||
}
|
||||
if (!window.requestAnimationFrame)
|
||||
window.requestAnimationFrame = function (callback, element) {
|
||||
var currTime = new Date().getTime();
|
||||
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
||||
var id = window.setTimeout(function () {
|
||||
callback(currTime + timeToCall);
|
||||
}, timeToCall);
|
||||
lastTime = currTime + timeToCall;
|
||||
return id;
|
||||
};
|
||||
if (!window.cancelAnimationFrame)
|
||||
window.cancelAnimationFrame = function (id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
})();
|
||||
|
||||
var canvas,
|
||||
progressTimerId,
|
||||
fadeTimerId,
|
||||
currentProgress,
|
||||
showing,
|
||||
addEvent = function (elem, type, handler) {
|
||||
if (elem.addEventListener) elem.addEventListener(type, handler, false);
|
||||
else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
|
||||
else elem["on" + type] = handler;
|
||||
},
|
||||
options = {
|
||||
autoRun: true,
|
||||
barThickness: 3,
|
||||
barColors: {
|
||||
0: "rgba(26, 188, 156, .9)",
|
||||
".25": "rgba(52, 152, 219, .9)",
|
||||
".50": "rgba(241, 196, 15, .9)",
|
||||
".75": "rgba(230, 126, 34, .9)",
|
||||
"1.0": "rgba(211, 84, 0, .9)",
|
||||
},
|
||||
shadowBlur: 10,
|
||||
shadowColor: "rgba(0, 0, 0, .6)",
|
||||
className: null,
|
||||
},
|
||||
repaint = function () {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = options.barThickness * 5; // need space for shadow
|
||||
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.shadowBlur = options.shadowBlur;
|
||||
ctx.shadowColor = options.shadowColor;
|
||||
|
||||
var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
||||
for (var stop in options.barColors)
|
||||
lineGradient.addColorStop(stop, options.barColors[stop]);
|
||||
ctx.lineWidth = options.barThickness;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, options.barThickness / 2);
|
||||
ctx.lineTo(
|
||||
Math.ceil(currentProgress * canvas.width),
|
||||
options.barThickness / 2
|
||||
);
|
||||
ctx.strokeStyle = lineGradient;
|
||||
ctx.stroke();
|
||||
},
|
||||
createCanvas = function () {
|
||||
canvas = document.createElement("canvas");
|
||||
var style = canvas.style;
|
||||
style.position = "fixed";
|
||||
style.top = style.left = style.right = style.margin = style.padding = 0;
|
||||
style.zIndex = 100001;
|
||||
style.display = "none";
|
||||
if (options.className) canvas.classList.add(options.className);
|
||||
document.body.appendChild(canvas);
|
||||
addEvent(window, "resize", repaint);
|
||||
},
|
||||
topbar = {
|
||||
config: function (opts) {
|
||||
for (var key in opts)
|
||||
if (options.hasOwnProperty(key)) options[key] = opts[key];
|
||||
},
|
||||
show: function () {
|
||||
if (showing) return;
|
||||
showing = true;
|
||||
if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
|
||||
if (!canvas) createCanvas();
|
||||
canvas.style.opacity = 1;
|
||||
canvas.style.display = "block";
|
||||
topbar.progress(0);
|
||||
if (options.autoRun) {
|
||||
(function loop() {
|
||||
progressTimerId = window.requestAnimationFrame(loop);
|
||||
topbar.progress(
|
||||
"+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
|
||||
);
|
||||
})();
|
||||
}
|
||||
},
|
||||
progress: function (to) {
|
||||
if (typeof to === "undefined") return currentProgress;
|
||||
if (typeof to === "string") {
|
||||
to =
|
||||
(to.indexOf("+") >= 0 || to.indexOf("-") >= 0
|
||||
? currentProgress
|
||||
: 0) + parseFloat(to);
|
||||
}
|
||||
currentProgress = to > 1 ? 1 : to;
|
||||
repaint();
|
||||
return currentProgress;
|
||||
},
|
||||
hide: function () {
|
||||
if (!showing) return;
|
||||
showing = false;
|
||||
if (progressTimerId != null) {
|
||||
window.cancelAnimationFrame(progressTimerId);
|
||||
progressTimerId = null;
|
||||
}
|
||||
(function loop() {
|
||||
if (topbar.progress("+.1") >= 1) {
|
||||
canvas.style.opacity -= 0.05;
|
||||
if (canvas.style.opacity <= 0.05) {
|
||||
canvas.style.display = "none";
|
||||
fadeTimerId = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
fadeTimerId = window.requestAnimationFrame(loop);
|
||||
})();
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module === "object" && typeof module.exports === "object") {
|
||||
module.exports = topbar;
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
define(function () {
|
||||
return topbar;
|
||||
});
|
||||
} else {
|
||||
this.topbar = topbar;
|
||||
}
|
||||
}.call(this, window, document));
|
||||
|
|
@ -8,38 +8,45 @@
|
|||
import Config
|
||||
|
||||
config :kink_bio,
|
||||
ecto_repos: [KinkBio.Repo]
|
||||
ecto_repos: [KinkBio.Repo],
|
||||
generators: [timestamp_type: :utc_datetime]
|
||||
|
||||
# Configures the endpoint
|
||||
config :kink_bio, KinkBioWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
render_errors: [view: KinkBioWeb.ErrorView, accepts: ~w(html json), layout: false],
|
||||
adapter: Bandit.PhoenixAdapter,
|
||||
render_errors: [
|
||||
formats: [html: KinkBioWeb.ErrorHTML, json: KinkBioWeb.ErrorJSON],
|
||||
layout: false
|
||||
],
|
||||
pubsub_server: KinkBio.PubSub,
|
||||
live_view: [signing_salt: "rwrgcNhG"]
|
||||
|
||||
config :kink_bio, KinkBio.Mailer, adapter: Swoosh.Adapters.Local
|
||||
|
||||
# Configure esbuild (the version is required)
|
||||
config :esbuild,
|
||||
version: "0.14.29",
|
||||
default: [
|
||||
version: "0.25.4",
|
||||
kink_bio: [
|
||||
args:
|
||||
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
||||
~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
|
||||
cd: Path.expand("../assets", __DIR__),
|
||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
|
||||
]
|
||||
|
||||
config :tailwind,
|
||||
version: "3.4.3",
|
||||
default: [
|
||||
version: "4.1.12",
|
||||
kink_bio: [
|
||||
# --config=tailwind.config.js
|
||||
args: ~w(
|
||||
--config=tailwind.config.js
|
||||
--input=css/app.css
|
||||
--output=../priv/static/assets/app.css
|
||||
--input=assets/css/app.css
|
||||
--output=priv/static/assets/css/app.css
|
||||
),
|
||||
cd: Path.expand("../assets", __DIR__)
|
||||
cd: Path.expand("..", __DIR__)
|
||||
]
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
config :logger, :default_formatter,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
metadata: [:request_id]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import Config
|
||||
|
||||
# Configure your database
|
||||
# config :kink_bio, KinkBio.Repo,
|
||||
# username: "postgres",
|
||||
# password: "postgres",
|
||||
|
|
@ -10,66 +9,34 @@ import Config
|
|||
# show_sensitive_data_on_connection_error: true,
|
||||
# pool_size: 10
|
||||
|
||||
# For development, we disable any cache and enable
|
||||
# debugging and code reloading.
|
||||
#
|
||||
# The watchers configuration can be used to run external
|
||||
# watchers to your application. For example, we use it
|
||||
# with esbuild to bundle .js and .css sources.
|
||||
config :kink_bio, KinkBioWeb.Endpoint,
|
||||
# Binding to loopback ipv4 address prevents access from other machines.
|
||||
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
|
||||
http: [ip: {0, 0, 0, 0}, port: 4000],
|
||||
http: [ip: {127, 0, 0, 1}],
|
||||
check_origin: false,
|
||||
code_reloader: true,
|
||||
debug_errors: true,
|
||||
secret_key_base: "grvuntf3V3MDygkTzkTaRU4X8/FgMRLN/DJv01CTUSAfPZ2x/y1eNZZuV0L51/lj",
|
||||
watchers: [
|
||||
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
|
||||
esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
|
||||
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
|
||||
esbuild: {Esbuild, :install_and_run, [:kink_bio, ~w(--sourcemap=inline --watch)]},
|
||||
tailwind: {Tailwind, :install_and_run, [:kink_bio, ~w(--watch)]}
|
||||
]
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# In order to use HTTPS in development, a self-signed
|
||||
# certificate can be generated by running the following
|
||||
# Mix task:
|
||||
#
|
||||
# mix phx.gen.cert
|
||||
#
|
||||
# Note that this task requires Erlang/OTP 20 or later.
|
||||
# Run `mix help phx.gen.cert` for more information.
|
||||
#
|
||||
# The `http:` config above can be replaced with:
|
||||
#
|
||||
# https: [
|
||||
# port: 4001,
|
||||
# cipher_suite: :strong,
|
||||
# keyfile: "priv/cert/selfsigned_key.pem",
|
||||
# certfile: "priv/cert/selfsigned.pem"
|
||||
# ],
|
||||
#
|
||||
# If desired, both `http:` and `https:` keys can be
|
||||
# configured to run both http and https servers on
|
||||
# different ports.
|
||||
|
||||
# Watch static and templates for browser reloading.
|
||||
config :kink_bio, KinkBioWeb.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||
~r"lib/kink_bio_web/(live|views)/.*(ex)$",
|
||||
~r"lib/kink_bio_web/templates/.*(eex)$"
|
||||
~r"priv/static/(?!uploads/).*\.(js|css|png|jpeg|jpg|gif|svg)$"E,
|
||||
~r"lib/kink_bio_web/router\.ex$"E,
|
||||
~r"lib/kink_bio_web/(controllers|live|components)/.*\.(ex|heex)$"E
|
||||
]
|
||||
]
|
||||
|
||||
# Do not include metadata nor timestamps in development logs
|
||||
config :logger, :console, format: "[$level] $message\n"
|
||||
|
||||
# Set a higher stacktrace during development. Avoid configuring such
|
||||
# in production as building large stacktraces may be expensive.
|
||||
config :kink_bio, dev_routes: true
|
||||
config :logger, :default_formatter, format: "[$level] $message\n"
|
||||
config :phoenix, :stacktrace_depth, 20
|
||||
|
||||
# Initialize plugs at runtime for faster development compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
|
||||
config :phoenix_live_view,
|
||||
debug_heex_annotations: true,
|
||||
debug_attributes: true,
|
||||
enable_expensive_runtime_checks: true
|
||||
|
||||
config :swoosh, :api_client, false
|
||||
|
|
|
|||
|
|
@ -1,49 +1,13 @@
|
|||
import Config
|
||||
|
||||
# For production, don't forget to configure the url host
|
||||
# to something meaningful, Phoenix uses this information
|
||||
# when generating URLs.
|
||||
#
|
||||
# Note we also include the path to a cache manifest
|
||||
# containing the digested version of static files. This
|
||||
# manifest is generated by the `mix phx.digest` task,
|
||||
# which you should run after static files are built and
|
||||
# before starting your production server.
|
||||
config :kink_bio, KinkBioWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
|
||||
|
||||
# Do not print debug messages in production
|
||||
config :logger, level: :info
|
||||
config :kink_bio, KinkBioWeb.Endpoint,
|
||||
force_ssl: [rewrite_on: [:x_forwarded_proto]],
|
||||
exclude: [
|
||||
hosts: ["localhost", "127.0.0.1"]
|
||||
]
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# To get SSL working, you will need to add the `https` key
|
||||
# to the previous section and set your `:url` port to 443:
|
||||
#
|
||||
# config :kink_bio, KinkBioWeb.Endpoint,
|
||||
# ...,
|
||||
# url: [host: "example.com", port: 443],
|
||||
# https: [
|
||||
# ...,
|
||||
# port: 443,
|
||||
# cipher_suite: :strong,
|
||||
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
|
||||
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
|
||||
# ]
|
||||
#
|
||||
# The `cipher_suite` is set to `:strong` to support only the
|
||||
# latest and more secure SSL ciphers. This means old browsers
|
||||
# and clients may not be supported. You can set it to
|
||||
# `:compatible` for wider support.
|
||||
#
|
||||
# `:keyfile` and `:certfile` expect an absolute path to the key
|
||||
# and cert in disk or a relative path inside priv, for example
|
||||
# "priv/ssl/server.key". For all supported SSL configuration
|
||||
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
|
||||
#
|
||||
# We also recommend setting `force_ssl` in your endpoint, ensuring
|
||||
# no data is ever sent via http, always redirecting to https:
|
||||
#
|
||||
# config :kink_bio, KinkBioWeb.Endpoint,
|
||||
# force_ssl: [hsts: true]
|
||||
#
|
||||
# Check `Plug.SSL` for all available options in `force_ssl`.
|
||||
config :swoosh, api_client: Swoosh.ApiClient.Req
|
||||
config :swoosh, local: false
|
||||
config :logger, level: :info
|
||||
|
|
|
|||
|
|
@ -1,25 +1,12 @@
|
|||
import Config
|
||||
|
||||
# config/runtime.exs is executed for all environments, including
|
||||
# during releases. It is executed after compilation and before the
|
||||
# system starts, so it is typically used to load production configuration
|
||||
# and secrets from environment variables or elsewhere. Do not define
|
||||
# any compile-time configuration in here, as it won't be applied.
|
||||
# The block below contains prod specific runtime configuration.
|
||||
|
||||
# ## Using releases
|
||||
#
|
||||
# If you use `mix release`, you need to explicitly enable the server
|
||||
# by passing the PHX_SERVER=true when you start it:
|
||||
#
|
||||
# PHX_SERVER=true bin/kink_bio start
|
||||
#
|
||||
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
|
||||
# script that automatically sets the env var above.
|
||||
if System.get_env("PHX_SERVER") do
|
||||
config :kink_bio, KinkBioWeb.Endpoint, server: true
|
||||
end
|
||||
|
||||
config :kink_bio, KinkBioWeb.Endpoint,
|
||||
http: [port: String.to_integer(System.get_env("PORT", "4000"))]
|
||||
|
||||
if config_env() == :prod do
|
||||
# database_url =
|
||||
# System.get_env("DATABASE_URL") ||
|
||||
|
|
@ -34,13 +21,10 @@ if config_env() == :prod do
|
|||
# # ssl: true,
|
||||
# url: database_url,
|
||||
# pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
||||
# # For machines with several cores, consider starting multiple pools of `pool_size`
|
||||
# # pool_count: 4,
|
||||
# socket_options: maybe_ipv6
|
||||
|
||||
# The secret key base is used to sign/encrypt cookies and other secrets.
|
||||
# A default value is used in config/dev.exs and config/test.exs but you
|
||||
# want to use a different value for prod and you most likely don't want
|
||||
# to check this value into version control, so we use an environment
|
||||
# variable instead.
|
||||
secret_key_base =
|
||||
System.get_env("SECRET_KEY_BASE") ||
|
||||
raise """
|
||||
|
|
@ -49,17 +33,13 @@ if config_env() == :prod do
|
|||
"""
|
||||
|
||||
host = System.get_env("PHX_HOST") || "example.com"
|
||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||
|
||||
config :kink_bio, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
||||
|
||||
config :kink_bio, KinkBioWeb.Endpoint,
|
||||
url: [host: host, port: 443, scheme: "https"],
|
||||
http: [
|
||||
# Enable IPv6 and bind on all interfaces.
|
||||
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
|
||||
# See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
|
||||
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
|
||||
ip: {0, 0, 0, 0, 0, 0, 0, 0},
|
||||
port: port
|
||||
ip: {0, 0, 0, 0, 0, 0, 0, 1}
|
||||
],
|
||||
secret_key_base: secret_key_base
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,27 +1,23 @@
|
|||
import Config
|
||||
|
||||
# Configure your database
|
||||
#
|
||||
# The MIX_TEST_PARTITION environment variable can be used
|
||||
# to provide built-in test partitioning in CI environment.
|
||||
# Run `mix help test` for more information.
|
||||
config :kink_bio, KinkBio.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
hostname: "localhost",
|
||||
database: "kink_bio_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
pool_size: 10
|
||||
pool_size: System.schedulers_online() * 2
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
config :kink_bio, KinkBioWeb.Endpoint,
|
||||
http: [ip: {127, 0, 0, 1}, port: 4002],
|
||||
secret_key_base: "iqFfo4HxtRE5XdN9w7dgA8SjT//2ntTHyBSGNchemZsG33kyFv+ZH5vvLxX5qfBP",
|
||||
server: false
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :kink_bio, KinkBio.Mailer, adapter: Swoosh.Adapters.Test
|
||||
config :swoosh, :api_client, false
|
||||
|
||||
config :logger, level: :warn
|
||||
|
||||
# Initialize plugs at runtime for faster test compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
config :phoenix_live_view, enable_expensive_runtime_checks: true
|
||||
config :phoenix, sort_verified_routes_query_params: true
|
||||
|
|
|
|||
|
|
@ -8,17 +8,10 @@ defmodule KinkBio.Application do
|
|||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# Start the Ecto repository
|
||||
# KinkBio.Repo,
|
||||
# https://jianjye.medium.com/how-to-disable-ecto-in-elixir-phoenix-after-project-creation-baa86582393a
|
||||
# Start the Telemetry supervisor
|
||||
KinkBioWeb.Telemetry,
|
||||
# Start the PubSub system
|
||||
{DNSCluster, query: Application.get_env(:kink_bio, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: KinkBio.PubSub},
|
||||
# Start the Endpoint (http/https)
|
||||
KinkBioWeb.Endpoint,
|
||||
# Start a worker by calling: KinkBio.Worker.start_link(arg)
|
||||
# {KinkBio.Worker, arg}
|
||||
{KinkBio.Cache, 86_400}
|
||||
]
|
||||
|
||||
|
|
|
|||
3
lib/kink_bio/mailer.ex
Normal file
3
lib/kink_bio/mailer.ex
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
defmodule KinkBio.Mailer do
|
||||
use Swoosh.Mailer, otp_app: :kink_bio
|
||||
end
|
||||
|
|
@ -1,74 +1,27 @@
|
|||
defmodule KinkBioWeb do
|
||||
@moduledoc """
|
||||
The entrypoint for defining your web interface, such
|
||||
as controllers, views, channels and so on.
|
||||
as controllers, components, channels, and so on.
|
||||
|
||||
This can be used in your application as:
|
||||
|
||||
use KinkBioWeb, :controller
|
||||
use KinkBioWeb, :view
|
||||
use KinkBioWeb, :html
|
||||
|
||||
The definitions below will be executed for every view,
|
||||
controller, etc, so keep them short and clean, focused
|
||||
The definitions below will be executed for every controller,
|
||||
component, etc, so keep them short and clean, focused
|
||||
on imports, uses and aliases.
|
||||
|
||||
Do NOT define functions inside the quoted expressions
|
||||
below. Instead, define any helper function in modules
|
||||
and import those modules here.
|
||||
below. Instead, define additional modules and import
|
||||
those modules here.
|
||||
"""
|
||||
|
||||
def controller do
|
||||
quote do
|
||||
use Phoenix.Controller, namespace: KinkBioWeb
|
||||
|
||||
import Plug.Conn
|
||||
alias KinkBioWeb.Router.Helpers, as: Routes
|
||||
end
|
||||
end
|
||||
|
||||
def view do
|
||||
quote do
|
||||
use Phoenix.View,
|
||||
root: "lib/kink_bio_web/templates",
|
||||
namespace: KinkBioWeb
|
||||
|
||||
# Import convenience functions from controllers
|
||||
import Phoenix.Controller,
|
||||
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
|
||||
|
||||
# Include shared imports and aliases for views
|
||||
unquote(view_helpers())
|
||||
end
|
||||
end
|
||||
|
||||
def live_view do
|
||||
quote do
|
||||
use Phoenix.LiveView,
|
||||
layout: {KinkBioWeb.LayoutView, "live.html"}
|
||||
|
||||
unquote(view_helpers())
|
||||
end
|
||||
end
|
||||
|
||||
def live_component do
|
||||
quote do
|
||||
use Phoenix.LiveComponent
|
||||
|
||||
unquote(view_helpers())
|
||||
end
|
||||
end
|
||||
|
||||
def component do
|
||||
quote do
|
||||
use Phoenix.Component
|
||||
|
||||
unquote(view_helpers())
|
||||
end
|
||||
end
|
||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
|
||||
|
||||
def router do
|
||||
quote do
|
||||
use Phoenix.Router
|
||||
use Phoenix.Router, helpers: false
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
|
|
@ -82,19 +35,65 @@ defmodule KinkBioWeb do
|
|||
end
|
||||
end
|
||||
|
||||
defp view_helpers do
|
||||
def controller do
|
||||
quote do
|
||||
# Use all HTML functionality (forms, tags, etc)
|
||||
use Phoenix.HTML
|
||||
use Phoenix.Controller, formats: [:html, :json]
|
||||
|
||||
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
|
||||
import Phoenix.LiveView.Helpers
|
||||
import Plug.Conn
|
||||
|
||||
# Import basic rendering functionality (render, render_layout, etc)
|
||||
import Phoenix.View
|
||||
unquote(verified_routes())
|
||||
end
|
||||
end
|
||||
|
||||
import KinkBioWeb.ErrorHelpers
|
||||
alias KinkBioWeb.Router.Helpers, as: Routes
|
||||
def live_view do
|
||||
quote do
|
||||
use Phoenix.LiveView
|
||||
|
||||
unquote(html_helpers())
|
||||
end
|
||||
end
|
||||
|
||||
def live_component do
|
||||
quote do
|
||||
use Phoenix.LiveComponent
|
||||
|
||||
unquote(html_helpers())
|
||||
end
|
||||
end
|
||||
|
||||
def html do
|
||||
quote do
|
||||
use Phoenix.Component
|
||||
|
||||
# Import convenience functions from controllers
|
||||
import Phoenix.Controller,
|
||||
only: [get_csrf_token: 0, view_module: 1, view_template: 1]
|
||||
|
||||
# Include shared imports and aliases for views
|
||||
unquote(html_helpers())
|
||||
end
|
||||
end
|
||||
|
||||
defp html_helpers do
|
||||
quote do
|
||||
# HTML escaping functionality
|
||||
import Phoenix.HTML
|
||||
# Core UI components
|
||||
import KinkBioWeb.CoreComponents
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
alias KinkBioWeb.Layouts
|
||||
|
||||
unquote(verified_routes())
|
||||
end
|
||||
end
|
||||
|
||||
def verified_routes do
|
||||
quote do
|
||||
use Phoenix.VerifiedRoutes,
|
||||
endpoint: KinkBioWeb.Endpoint,
|
||||
router: KinkBioWeb.Router,
|
||||
statics: KinkBioWeb.static_paths()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
455
lib/kink_bio_web/components/core_components.ex
Normal file
455
lib/kink_bio_web/components/core_components.ex
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
defmodule KinkBioWeb.CoreComponents do
|
||||
@moduledoc """
|
||||
Provides core UI components.
|
||||
|
||||
At first glance, this module may seem daunting, but its goal is to provide
|
||||
core building blocks for your application, such as tables, forms, and
|
||||
inputs. The components consist mostly of markup and are well-documented
|
||||
with doc strings and declarative assigns. You may customize and style
|
||||
them in any way you want, based on your application growth and needs.
|
||||
|
||||
The foundation for styling is Tailwind CSS, a utility-first CSS framework,
|
||||
augmented with daisyUI, a Tailwind CSS plugin that provides UI components
|
||||
and themes. Here are useful references:
|
||||
|
||||
* [daisyUI](https://daisyui.com/docs/intro/) - a good place to get
|
||||
started and see the available components.
|
||||
|
||||
* [Tailwind CSS](https://tailwindcss.com) - the foundational framework
|
||||
we build on. You will use it for layout, sizing, flexbox, grid, and
|
||||
spacing.
|
||||
|
||||
* [Phoenix.Component](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) -
|
||||
the component system used by Phoenix. Some components, such as `<.link>`
|
||||
and `<.form>`, are defined there.
|
||||
|
||||
"""
|
||||
use Phoenix.Component
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
@doc """
|
||||
Renders flash notices.
|
||||
|
||||
## Examples
|
||||
|
||||
<.flash kind={:info} flash={@flash} />
|
||||
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
|
||||
"""
|
||||
attr :id, :string, doc: "the optional id of flash container"
|
||||
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
|
||||
attr :title, :string, default: nil
|
||||
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
|
||||
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
|
||||
|
||||
slot :inner_block, doc: "the optional inner block that renders the flash message"
|
||||
|
||||
def flash(assigns) do
|
||||
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
|
||||
id={@id}
|
||||
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
|
||||
role="alert"
|
||||
class="toast toast-top toast-end z-50"
|
||||
{@rest}
|
||||
>
|
||||
<div class={
|
||||
[
|
||||
"alert w-80 sm:w-96 max-w-80 sm:max-w-96 text-wrap",
|
||||
@kind == :info && "alert-info",
|
||||
@kind == :error && "alert-error"
|
||||
]
|
||||
}>
|
||||
<.icon :if={@kind == :info} name="circle-info" class="size-5 shrink-0" />
|
||||
<.icon :if={@kind == :error} name="circle-exclamation" class="size-5 shrink-0" />
|
||||
<div>
|
||||
<p :if={@title} class="font-semibold">{@title}</p>
|
||||
<p>{msg}</p>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
<button type="button" class="group self-start cursor-pointer" aria-label="close">
|
||||
<.icon name="xmark" class="size-5 opacity-40 group-hover:opacity-70" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a button with navigation support.
|
||||
|
||||
## Examples
|
||||
|
||||
<.button>Send!</.button>
|
||||
<.button phx-click="go" variant="primary">Send!</.button>
|
||||
<.button navigate={~p"/"}>Home</.button>
|
||||
"""
|
||||
attr :rest, :global, include: ~w(href navigate patch method download name value disabled)
|
||||
attr :class, :any
|
||||
attr :variant, :string, values: ~w(primary)
|
||||
slot :inner_block, required: true
|
||||
|
||||
def button(%{rest: rest} = assigns) do
|
||||
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
|
||||
|
||||
assigns =
|
||||
assign_new(assigns, :class, fn ->
|
||||
["btn", Map.fetch!(variants, assigns[:variant])]
|
||||
end)
|
||||
|
||||
if rest[:href] || rest[:navigate] || rest[:patch] do
|
||||
~H"""
|
||||
<.link class={@class} {@rest}>
|
||||
{render_slot(@inner_block)}
|
||||
</.link>
|
||||
"""
|
||||
else
|
||||
~H"""
|
||||
<button class={@class} {@rest}>
|
||||
{render_slot(@inner_block)}
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders an input with label and error messages.
|
||||
|
||||
A `Phoenix.HTML.FormField` may be passed as argument,
|
||||
which is used to retrieve the input name, id, and values.
|
||||
Otherwise all attributes may be passed explicitly.
|
||||
|
||||
## Types
|
||||
|
||||
This function accepts all HTML input types, considering that:
|
||||
|
||||
* You may also set `type="select"` to render a `<select>` tag
|
||||
|
||||
* `type="checkbox"` is used exclusively to render boolean values
|
||||
|
||||
* For live file uploads, see `Phoenix.Component.live_file_input/1`
|
||||
|
||||
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
|
||||
for more information. Unsupported types, such as radio, are best
|
||||
written directly in your templates.
|
||||
|
||||
## Examples
|
||||
|
||||
```heex
|
||||
<.input field={@form[:email]} type="email" />
|
||||
<.input name="my-input" errors={["oh no!"]} />
|
||||
```
|
||||
|
||||
## Select type
|
||||
|
||||
When using `type="select"`, you must pass the `options` and optionally
|
||||
a `value` to mark which option should be preselected.
|
||||
|
||||
```heex
|
||||
<.input field={@form[:user_type]} type="select" options={["Admin": "admin", "User": "user"]} />
|
||||
```
|
||||
|
||||
For more information on what kind of data can be passed to `options` see
|
||||
[`options_for_select`](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#options_for_select/2).
|
||||
"""
|
||||
attr :id, :any, default: nil
|
||||
attr :name, :any
|
||||
attr :label, :string, default: nil
|
||||
attr :value, :any
|
||||
|
||||
attr :type, :string,
|
||||
default: "text",
|
||||
values: ~w(checkbox color date datetime-local email file month number password
|
||||
search select tel text textarea time url week hidden)
|
||||
|
||||
attr :field, Phoenix.HTML.FormField,
|
||||
doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
||||
|
||||
attr :errors, :list, default: []
|
||||
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
|
||||
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
|
||||
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
|
||||
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
|
||||
attr :class, :any, default: nil, doc: "the input class to use over defaults"
|
||||
attr :error_class, :any, default: nil, doc: "the input error class to use over defaults"
|
||||
|
||||
attr :rest, :global,
|
||||
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
|
||||
multiple pattern placeholder readonly required rows size step)
|
||||
|
||||
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
|
||||
|
||||
assigns
|
||||
|> assign(field: nil, id: assigns.id || field.id)
|
||||
|> assign(:errors, errors)
|
||||
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|
||||
|> assign_new(:value, fn -> field.value end)
|
||||
|> input()
|
||||
end
|
||||
|
||||
def input(%{type: "hidden"} = assigns) do
|
||||
~H"""
|
||||
<input type="hidden" id={@id} name={@name} value={@value} {@rest} />
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "checkbox"} = assigns) do
|
||||
assigns =
|
||||
assign_new(assigns, :checked, fn ->
|
||||
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
|
||||
end)
|
||||
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
<label>
|
||||
<input
|
||||
type="hidden"
|
||||
name={@name}
|
||||
value="false"
|
||||
disabled={@rest[:disabled]}
|
||||
form={@rest[:form]}
|
||||
/>
|
||||
<span class="label">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={@id}
|
||||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class={@class || "checkbox checkbox-sm"}
|
||||
{@rest}
|
||||
/>{@label}
|
||||
</span>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}>{msg}</.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "select"} = assigns) do
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
<label>
|
||||
<span :if={@label} class="label mb-1">{@label}</span>
|
||||
<select
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[@class || "w-full select", @errors != [] && (@error_class || "select-error")]}
|
||||
multiple={@multiple}
|
||||
{@rest}
|
||||
>
|
||||
<option :if={@prompt} value="">{@prompt}</option>
|
||||
{Phoenix.HTML.Form.options_for_select(@options, @value)}
|
||||
</select>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}>{msg}</.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "textarea"} = assigns) do
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
<label>
|
||||
<span :if={@label} class="label mb-1">{@label}</span>
|
||||
<textarea
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={
|
||||
[
|
||||
@class || "w-full textarea",
|
||||
@errors != [] && (@error_class || "textarea-error")
|
||||
]
|
||||
}
|
||||
{@rest}
|
||||
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}>{msg}</.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
||||
def input(assigns) do
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
<label>
|
||||
<span :if={@label} class="label mb-1">{@label}</span>
|
||||
<input
|
||||
type={@type}
|
||||
name={@name}
|
||||
id={@id}
|
||||
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
||||
class={
|
||||
[
|
||||
@class || "w-full input",
|
||||
@errors != [] && (@error_class || "input-error")
|
||||
]
|
||||
}
|
||||
{@rest}
|
||||
/>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}>{msg}</.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Helper used by inputs to generate form errors
|
||||
defp error(assigns) do
|
||||
~H"""
|
||||
<p class="mt-1.5 flex gap-2 items-center text-sm text-error">
|
||||
<.icon name="circle-exclamation" class="size-5" /> {render_slot(@inner_block)}
|
||||
</p>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a header with title.
|
||||
"""
|
||||
slot :inner_block, required: true
|
||||
slot :subtitle
|
||||
slot :actions
|
||||
|
||||
def header(assigns) do
|
||||
~H"""
|
||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4"]}>
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold leading-8">
|
||||
{render_slot(@inner_block)}
|
||||
</h1>
|
||||
<p :if={@subtitle != []} class="text-sm text-base-content/70">
|
||||
{render_slot(@subtitle)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-none">{render_slot(@actions)}</div>
|
||||
</header>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a table with generic styling.
|
||||
|
||||
## Examples
|
||||
|
||||
<.table id="users" rows={@users}>
|
||||
<:col :let={user} label="id">{user.id}</:col>
|
||||
<:col :let={user} label="username">{user.username}</:col>
|
||||
</.table>
|
||||
"""
|
||||
attr :id, :string, required: true
|
||||
attr :rows, :list, required: true
|
||||
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
|
||||
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
|
||||
|
||||
attr :row_item, :any,
|
||||
default: &Function.identity/1,
|
||||
doc: "the function for mapping each row before calling the :col and :action slots"
|
||||
|
||||
slot :col, required: true do
|
||||
attr :label, :string
|
||||
end
|
||||
|
||||
slot :action, doc: "the slot for showing user actions in the last table column"
|
||||
|
||||
def table(assigns) do
|
||||
assigns =
|
||||
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
|
||||
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
|
||||
end
|
||||
|
||||
~H"""
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :for={col <- @col}>{col[:label]}</th>
|
||||
<th :if={@action != []}>
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
|
||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
|
||||
<td
|
||||
:for={col <- @col}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={@row_click && "hover:cursor-pointer"}
|
||||
>
|
||||
{render_slot(col, @row_item.(row))}
|
||||
</td>
|
||||
<td :if={@action != []} class="w-0 font-semibold">
|
||||
<div class="flex gap-4">
|
||||
<%= for action <- @action do %>
|
||||
{render_slot(action, @row_item.(row))}
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a data list.
|
||||
|
||||
## Examples
|
||||
|
||||
<.list>
|
||||
<:item title="Title">{@post.title}</:item>
|
||||
<:item title="Views">{@post.views}</:item>
|
||||
</.list>
|
||||
"""
|
||||
slot :item, required: true do
|
||||
attr :title, :string, required: true
|
||||
end
|
||||
|
||||
def list(assigns) do
|
||||
~H"""
|
||||
<ul class="list">
|
||||
<li :for={item <- @item} class="list-row">
|
||||
<div class="list-col-grow">
|
||||
<div class="font-bold">{item.title}</div>
|
||||
<div>{render_slot(item)}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :name, :string, required: true
|
||||
attr :style, :string, default: "regular"
|
||||
attr :class, :any, default: ""
|
||||
|
||||
def icon(assigns) do
|
||||
~H"""
|
||||
<i class={["fa-#{@name}", "fa-#{@style}", @class]}></i>
|
||||
"""
|
||||
end
|
||||
|
||||
## JS Commands
|
||||
|
||||
def show(js \\ %JS{}, selector) do
|
||||
JS.show(js,
|
||||
to: selector,
|
||||
time: 300,
|
||||
transition:
|
||||
{"transition-all ease-out duration-300",
|
||||
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
|
||||
"opacity-100 translate-y-0 sm:scale-100"}
|
||||
)
|
||||
end
|
||||
|
||||
def hide(js \\ %JS{}, selector) do
|
||||
JS.hide(js,
|
||||
to: selector,
|
||||
time: 200,
|
||||
transition:
|
||||
{"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100",
|
||||
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
|
||||
)
|
||||
end
|
||||
end
|
||||
88
lib/kink_bio_web/components/layouts.ex
Normal file
88
lib/kink_bio_web/components/layouts.ex
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
defmodule KinkBioWeb.Layouts do
|
||||
@moduledoc """
|
||||
This module holds layouts and related functionality
|
||||
used by your application.
|
||||
"""
|
||||
use KinkBioWeb, :html
|
||||
|
||||
# Embed all files in layouts/* within this module.
|
||||
# The default root.html.heex file contains the HTML
|
||||
# skeleton of your application, namely HTML headers
|
||||
# and other static content.
|
||||
embed_templates "layouts/*"
|
||||
|
||||
@doc """
|
||||
Renders your app layout.
|
||||
|
||||
This function is typically invoked from every template,
|
||||
and it often contains your application menu, sidebar,
|
||||
or similar.
|
||||
|
||||
## Examples
|
||||
|
||||
<Layouts.app flash={@flash}>
|
||||
<h1>Content</h1>
|
||||
</Layouts.app>
|
||||
|
||||
"""
|
||||
attr :flash, :map, required: true, doc: "the map of flash messages"
|
||||
|
||||
attr :current_scope, :map,
|
||||
default: nil,
|
||||
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def app(assigns) do
|
||||
~H"""
|
||||
<main class="h-full mx-auto max-w-5xl px-4">
|
||||
{render_slot(@inner_block)}
|
||||
</main>
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shows the flash group with standard titles and content.
|
||||
|
||||
## Examples
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
"""
|
||||
attr :flash, :map, required: true, doc: "the map of flash messages"
|
||||
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
||||
|
||||
def flash_group(assigns) do
|
||||
~H"""
|
||||
<div id={@id} aria-live="polite">
|
||||
<.flash kind={:info} flash={@flash} />
|
||||
<.flash kind={:error} flash={@flash} />
|
||||
|
||||
<.flash
|
||||
id="client-error"
|
||||
kind={:error}
|
||||
title="We can't find the internet"
|
||||
phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
|
||||
phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
|
||||
hidden
|
||||
>
|
||||
Attempting to reconnect
|
||||
<.icon name="arrows-rotate" class="ml-1 size-3 motion-safe:animate-spin" />
|
||||
</.flash>
|
||||
|
||||
<.flash
|
||||
id="server-error"
|
||||
kind={:error}
|
||||
title="Something went wrong!"
|
||||
phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")}
|
||||
phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})}
|
||||
hidden
|
||||
>
|
||||
Attempting to reconnect
|
||||
<.icon name="arrows-rotate" class="ml-1 size-3 motion-safe:animate-spin" />
|
||||
</.flash>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="bg-base">
|
||||
<html lang="en" class="mocha bg-ctp-base" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta name="csrf-token" content={csrf_token_value()}>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="csrf-token" content={get_csrf_token()}>
|
||||
|
||||
<!-- pretty embeds -->
|
||||
<%= live_title_tag assigns[:page_title] || "kink.bio" %>
|
||||
<.live_title default="kink.bio">
|
||||
{assigns[:page_title]}
|
||||
</.live_title>
|
||||
<meta name="description" content="simple, private, no-bullshit kink sharing"/>
|
||||
<meta name="theme-color" content="#cba6f7" />
|
||||
|
||||
|
|
@ -30,10 +31,10 @@
|
|||
<script src="https://kit.fontawesome.com/d97b8f3c5a.js" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- phoenix -->
|
||||
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
|
||||
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}></script>
|
||||
</head>
|
||||
<body class="min-h-screen min-w-screen bg-base text-text font-body py-12">
|
||||
<%= @inner_content %>
|
||||
<body class="bg-ctp-base text-ctp-text font-body py-12">
|
||||
{@inner_content}
|
||||
</body>
|
||||
</html>
|
||||
24
lib/kink_bio_web/controllers/error_html.ex
Normal file
24
lib/kink_bio_web/controllers/error_html.ex
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
defmodule KinkBioWeb.ErrorHTML do
|
||||
@moduledoc """
|
||||
This module is invoked by your endpoint in case of errors on HTML requests.
|
||||
|
||||
See config/config.exs.
|
||||
"""
|
||||
use KinkBioWeb, :html
|
||||
|
||||
# If you want to customize your error pages,
|
||||
# uncomment the embed_templates/1 call below
|
||||
# and add pages to the error directory:
|
||||
#
|
||||
# * lib/kink_bio_web/controllers/error_html/404.html.heex
|
||||
# * lib/kink_bio_web/controllers/error_html/500.html.heex
|
||||
#
|
||||
# embed_templates "error_html/*"
|
||||
|
||||
# The default is to render a plain text page based on
|
||||
# the template name. For example, "404.html" becomes
|
||||
# "Not Found".
|
||||
def render(template, _assigns) do
|
||||
Phoenix.Controller.status_message_from_template(template)
|
||||
end
|
||||
end
|
||||
21
lib/kink_bio_web/controllers/error_json.ex
Normal file
21
lib/kink_bio_web/controllers/error_json.ex
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
defmodule KinkBioWeb.ErrorJSON do
|
||||
@moduledoc """
|
||||
This module is invoked by your endpoint in case of errors on JSON requests.
|
||||
|
||||
See config/config.exs.
|
||||
"""
|
||||
|
||||
# If you want to customize a particular status code,
|
||||
# you may add your own clauses, such as:
|
||||
#
|
||||
# def render("500.json", _assigns) do
|
||||
# %{errors: %{detail: "Internal Server Error"}}
|
||||
# end
|
||||
|
||||
# By default, Phoenix returns the status message from
|
||||
# the template name. For example, "404.json" becomes
|
||||
# "Not Found".
|
||||
def render(template, _assigns) do
|
||||
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
|
||||
end
|
||||
end
|
||||
|
|
@ -2,19 +2,15 @@ defmodule KinkBioWeb.PageController do
|
|||
use KinkBioWeb, :controller
|
||||
|
||||
def index(conn, _params) do
|
||||
render(conn, "index.html")
|
||||
render(conn, :index)
|
||||
end
|
||||
|
||||
def select(conn, _params) do
|
||||
render(conn, "selections.html")
|
||||
render(conn, :selections)
|
||||
end
|
||||
|
||||
def flow_public(conn, _params) do
|
||||
render(conn, "flow_public.html")
|
||||
end
|
||||
|
||||
def flow_private(conn, _params) do
|
||||
render(conn, "flow_private.html")
|
||||
render(conn, :flow_public)
|
||||
end
|
||||
|
||||
def flow_join(conn, params) do
|
||||
|
|
@ -26,6 +22,6 @@ defmodule KinkBioWeb.PageController do
|
|||
end
|
||||
|
||||
def view(conn, %{"id" => id}) do
|
||||
render(conn, "view.html", id: id)
|
||||
render(conn, :view, id: id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
10
lib/kink_bio_web/controllers/page_html.ex
Normal file
10
lib/kink_bio_web/controllers/page_html.ex
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
defmodule KinkBioWeb.PageHTML do
|
||||
@moduledoc """
|
||||
This module contains pages rendered by PageController.
|
||||
|
||||
See the `page_html` directory for all templates available.
|
||||
"""
|
||||
use KinkBioWeb, :html
|
||||
|
||||
embed_templates "page_html/*"
|
||||
end
|
||||
65
lib/kink_bio_web/controllers/page_html/flow_public.html.heex
Normal file
65
lib/kink_bio_web/controllers/page_html/flow_public.html.heex
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<section class="mb-12">
|
||||
<h1 class="from-ctp-pink to-ctp-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-linear-to-r font-extrabold font-display">publish your list</h1>
|
||||
</section>
|
||||
|
||||
<section class="mb-12 text-2xl flex flex-col md:flex-row">
|
||||
<button min-w-[50%] class="border border-solid border-ctp-surface1 bg-ctp-surface1 text-ctp-subtext0 font-bold py-2 px-4 max-md:rounded-t-lg md:rounded-l-lg transition-all ease-in-out duration-150" disabled onclick="view()" id="link">
|
||||
https://kink.bio/view
|
||||
</button>
|
||||
<button min-w-[50%] class="bg-linear-to-l from-ctp-red to-ctp-pink p-1 transition-all ease-in-out duration-500 bg-size-200 bg-pos-0 hover:bg-pos-100 text-ctp-crust font-bold py-2 px-4 md:rounded-r-lg max-md:rounded-b-lg md:w-40 cursor-pointer" onclick="publish(this)">
|
||||
Publish
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="text-ctp-subtext0 italic">
|
||||
This link will be valid for 24 hours. After that, it will be deleted.
|
||||
</section>
|
||||
</Layouts.app>
|
||||
|
||||
<script>
|
||||
const link_el = document.getElementById("link");
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const state = params.get("state");
|
||||
const url = `${location.origin}/view/${state}`;
|
||||
link_el.innerHTML = url;
|
||||
|
||||
link_el.classList.remove("bg-surface1");
|
||||
link_el.classList.add("bg-surface0");
|
||||
|
||||
link_el.classList.remove("text-subtext0");
|
||||
link_el.classList.add("text-text");
|
||||
|
||||
link_el.classList.remove("border-surface1");
|
||||
link_el.classList.add("border-pink");
|
||||
|
||||
link_el.classList.add("hover:bg-gradient-to-l");
|
||||
link_el.classList.add("hover:from-pink");
|
||||
link_el.classList.add("hover:to-mauve");
|
||||
link_el.classList.add("hover:text-surface0");
|
||||
|
||||
link_el.disabled = false;
|
||||
|
||||
function view(el) {
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
async function onCopy(el) {
|
||||
el.innerHTML = "Copied"
|
||||
el.classList.remove("duration-500");
|
||||
el.classList.remove("transition-500");
|
||||
el.classList.add("bg-green");
|
||||
el.classList.remove("bg-gradient-to-l");
|
||||
|
||||
link_el.classList.remove("border-surface1");
|
||||
link_el.classList.remove("border-pink");
|
||||
link_el.classList.add("border-green");
|
||||
|
||||
link_el.classList.remove("hover:from-pink");
|
||||
link_el.classList.remove("hover:to-mauve");
|
||||
link_el.classList.add("hover:from-green");
|
||||
link_el.classList.add("hover:to-sapphire");
|
||||
|
||||
navigator.clipboard.writeText(url);
|
||||
}
|
||||
</script>
|
||||
36
lib/kink_bio_web/controllers/page_html/index.html.heex
Normal file
36
lib/kink_bio_web/controllers/page_html/index.html.heex
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<section class="mb-12">
|
||||
<h1 class="from-ctp-pink to-ctp-mauve text-5xl md:text-7xl text-transparent bg-clip-text bg-linear-to-r font-extrabold font-display">create a link</h1>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 h-full mt-8 gap-4 select-none">
|
||||
<.link href={~p"/select?flow=private"}>
|
||||
<button type="button" class="rounded-lg bg-linear-to-tl from-ctp-sky via-ctp-mauve to-ctp-pink p-1 transition-all ease-in-out duration-500 bg-size-200 bg-pos-0 hover:bg-pos-100 aspect-square cursor-pointer">
|
||||
<div class="bg-ctp-base hover:bg-ctp-mantle transition-all ease-in-out duration-500 h-full w-full rounded-md">
|
||||
<div class="flex flex-col justify-center items-center h-full w-full p-12 md:p-24 m-auto gap-2">
|
||||
<i class="fa-duotone fa-key text-5xl"></i>
|
||||
<h3 class="mt-8 text-4xl font-bold text-ctp-text">private</h3>
|
||||
<p class="mt-1 text-lg text-ctp-subtext1">share kinks with a trusted person</p>
|
||||
<p class="text-sm text-ctp-subtext0 text-center">this will generate a one-time link for you to send to your trustee – only kinks you two have in common will be shown</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</.link>
|
||||
<.link href={~p"/select?flow=public"}>
|
||||
<button type="button" class="rounded-lg bg-linear-to-tl from-ctp-green via-ctp-sky to-ctp-mauve p-1 transition-all ease-in-out duration-500 bg-size-200 bg-pos-0 hover:bg-pos-100 aspect-square cursor-pointer">
|
||||
<div class="bg-ctp-base hover:bg-ctp-mantle transition-all ease-in-out duration-500 h-full w-full rounded-md">
|
||||
<div class="flex flex-col justify-center items-center h-full w-full p-12 md:p-24 m-auto gap-2">
|
||||
<i class="fa-duotone fa-earth-africa text-5xl"></i>
|
||||
<h3 class="mt-8 text-4xl font-bold text-ctp-text">public</h3>
|
||||
<p class="mt-1 text-lg text-ctp-subtext1">generate a publicly accessible link</p>
|
||||
<p class="text-sm text-ctp-subtext0 text-center">this will generate a public link to your kink list – it won't be visible without the link, but anyone with the link will be able to view it</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</.link>
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<p class="text-ctp-subtext0 italic">all links automatically expire after 24h</p>
|
||||
</section>
|
||||
</Layouts.app>
|
||||
|
|
@ -1,26 +1,27 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<section class="mb-12">
|
||||
<h1 class="from-pink to-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-gradient-to-r font-extrabold font-display">time to get kinky</h1>
|
||||
<h1 class="from-ctp-pink to-ctp-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-linear-to-r font-extrabold font-display">time to get kinky</h1>
|
||||
</section>
|
||||
|
||||
<section class="mb-8 flex flex-col gap-1 px-2 text-md text-subtext1">
|
||||
<section class="mb-8 flex flex-col gap-1 px-2 text-md text-ctp-subtext1">
|
||||
<div>
|
||||
<div class="fa-solid fa-circle-heart text-sky pr-1"></div>
|
||||
<i class="fa-light fa-dash text-overlay0 pr-1"></i>
|
||||
<div class="fa-solid fa-circle-heart text-ctp-sky pr-1"></div>
|
||||
<i class="fa-light fa-dash text-ctp-overlay0 pr-1"></i>
|
||||
i want to do or try this
|
||||
</div>
|
||||
<div>
|
||||
<div class="fa-solid fa-circle-check text-green pr-1"></div>
|
||||
<i class="fa-light fa-dash text-overlay0 pr-1"></i>
|
||||
<div class="fa-solid fa-circle-check text-ctp-green pr-1"></div>
|
||||
<i class="fa-light fa-dash text-ctp-overlay0 pr-1"></i>
|
||||
i am okay with doing or trying this
|
||||
</div>
|
||||
<div>
|
||||
<div class="fa-solid fa-circle-exclamation text-yellow pr-1"></div>
|
||||
<i class="fa-light fa-dash text-overlay0 pr-1"></i>
|
||||
<div class="fa-solid fa-circle-exclamation text-ctp-yellow pr-1"></div>
|
||||
<i class="fa-light fa-dash text-ctp-overlay0 pr-1"></i>
|
||||
i need to talk about this first
|
||||
</div>
|
||||
<div>
|
||||
<div class="fa-solid fa-circle-xmark text-red pr-1"></div>
|
||||
<i class="fa-light fa-dash text-overlay0 pr-1"></i>
|
||||
<div class="fa-solid fa-circle-xmark text-ctp-red pr-1"></div>
|
||||
<i class="fa-light fa-dash text-ctp-overlay0 pr-1"></i>
|
||||
i don't want to do this – no exceptions
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -28,42 +29,42 @@
|
|||
<section class="columns-1 md:columns-2 mt-8 mb-4 gap-4">
|
||||
<%= for {category, i} <- Enum.with_index(KinkBio.Kinklist.get) do %>
|
||||
<div style={"z-index: #{1 + length(KinkBio.Kinklist.get) - i};"} class="md:px-2 py-1 mb-4 break-inside-avoid relative">
|
||||
<h2 class="text-sm text-overlay2 mb-1 uppercase"><%= category["name"] %></h2>
|
||||
<h2 class="text-sm text-ctp-overlay2 mb-1 uppercase"><%= category["name"] %></h2>
|
||||
<div class="relative isolate -translate-x-1.5">
|
||||
<%= if category["spoiler"] do %>
|
||||
<div class="z-10 absolute w-full h-full flex flex-col justify-center items-center bg-base/95 backdrop-blur-sm cursor-pointer transition-all ease-in-out duration-150" onclick="this.classList.add('opacity-0'); setTimeout(() => this.classList.add('hidden'), 150)">
|
||||
<div class="z-10 absolute w-full h-full flex flex-col justify-center items-center bg-ctp-base/95 backdrop-blur-sm cursor-pointer transition-all ease-in-out duration-150" onclick="this.classList.add('opacity-0'); setTimeout(() => this.classList.add('hidden'), 150)">
|
||||
<i class="fa-duotone fa-circle-exclamation text-2xl"></i>
|
||||
<p class="text-center text-subtext1 max-w-[50%]">
|
||||
<p class="text-center text-ctp-subtext1 max-w-[50%]">
|
||||
trigger warning
|
||||
</p>
|
||||
<p class="text-center text-overlay0 max-w-[50%]">
|
||||
<p class="text-center text-ctp-overlay0 max-w-[50%]">
|
||||
click to reveal
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= for kink <- category["kinks"] do %>
|
||||
<div class="flex flex-row justify-between items-center text-lg md:text-xl hover:bg-mantle px-1.5 rounded-lg">
|
||||
<div class="flex flex-row justify-between items-center text-lg md:text-xl hover:bg-ctp-mantle px-1.5 rounded-lg">
|
||||
<div class="tooltip cursor-help" data-text={kink["description"]}>
|
||||
<%= kink["name"] %>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-1 leading-none">
|
||||
<div class="flex space-x-1 w-fit leading-none">
|
||||
<input type="radio" name={kink["name"]} id={"fav-#{kink["name"]}"} class="peer/fav hidden" />
|
||||
<input type="radio" name={kink["name"]} id={"fav-#{kink["id"]}"} class="peer/fav hidden" />
|
||||
<label
|
||||
for={"fav-#{kink["name"]}"}
|
||||
class="fa-circle-heart fa-sky fa-duotone fa-secondary-20 hover:fa-secondary-80 hover:fa-primary-0 peer-checked/fav:fa-secondary-100 peer-checked/fav:fa-primary-0 cursor-pointer select-none"></label>
|
||||
<input type="radio" name={kink["name"]} id={"yes-#{kink["name"]}"} class="peer/yes hidden" />
|
||||
class="fa-circle-heart fa-primary-ctp-sky fa-secondary-ctp-sky fa-duotone fa-secondary-20 hover:fa-secondary-80 hover:fa-primary-0 peer-checked/fav:fa-secondary-100 peer-checked/fav:fa-primary-0 cursor-pointer select-none"></label>
|
||||
<input type="radio" name={kink["name"]} id={"yes-#{kink["id"]}"} class="peer/yes hidden" />
|
||||
<label
|
||||
for={"yes-#{kink["name"]}"}
|
||||
class="fa-circle-check fa-green fa-duotone fa-secondary-20 hover:fa-secondary-80 hover:fa-primary-0 peer-checked/yes:fa-secondary-100 peer-checked/yes:fa-primary-0 cursor-pointer select-none"></label>
|
||||
<input type="radio" name={kink["name"]} id={"may-#{kink["name"]}"} class="peer/may hidden" checked={not category["spoiler"]} />
|
||||
class="fa-circle-check fa-primary-ctp-green fa-secondary-ctp-green fa-duotone fa-secondary-20 hover:fa-secondary-80 hover:fa-primary-0 peer-checked/yes:fa-secondary-100 peer-checked/yes:fa-primary-0 cursor-pointer select-none"></label>
|
||||
<input type="radio" name={kink["name"]} id={"may-#{kink["id"]}"} class="peer/may hidden" checked={not category["spoiler"]} />
|
||||
<label
|
||||
for={"may-#{kink["name"]}"}
|
||||
class="fa-circle-exclamation fa-yellow fa-duotone fa-secondary-20 hover:fa-secondary-80 hover:fa-primary-0 peer-checked/may:fa-secondary-100 peer-checked/may:fa-primary-0 cursor-pointer select-none"></label>
|
||||
<input type="radio" name={kink["name"]} id={"nah-#{kink["name"]}"} class="peer/nah hidden" checked={category["spoiler"]} />
|
||||
class="fa-circle-exclamation fa-primary-ctp-yellow fa-secondary-ctp-yellow fa-duotone fa-secondary-20 hover:fa-secondary-80 hover:fa-primary-0 peer-checked/may:fa-secondary-100 peer-checked/may:fa-primary-0 cursor-pointer select-none"></label>
|
||||
<input type="radio" name={kink["name"]} id={"nah-#{kink["id"]}"} class="peer/nah hidden" checked={category["spoiler"]} />
|
||||
<label
|
||||
for={"nah-#{kink["name"]}"}
|
||||
class="fa-circle-xmark fa-red fa-duotone fa-secondary-20 hover:fa-secondary-80 hover:fa-primary-0 peer-checked/nah:fa-secondary-100 peer-checked/nah:fa-primary-0 cursor-pointer select-none"></label>
|
||||
class="fa-circle-xmark fa-primary-ctp-red fa-secondary-ctp-red fa-duotone fa-secondary-20 hover:fa-secondary-80 hover:fa-primary-0 peer-checked/nah:fa-secondary-100 peer-checked/nah:fa-primary-0 cursor-pointer select-none"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -74,10 +75,11 @@
|
|||
</section>
|
||||
|
||||
<section class="flex flex-row items-center justify-end mr-4">
|
||||
<button class="bg-mauve hover:bg-pink transition rounded-md px-4 py-2 text-mantle" onclick="saveToIDB()">
|
||||
<button class="bg-ctp-mauve hover:bg-ctp-pink transition rounded-md px-4 py-2 text-ctp-mantle cursor-pointer" onclick="saveToIDB()">
|
||||
Save & Continue <i class="fa-solid fa-arrow-right ml-2"></i>
|
||||
</button>
|
||||
</section>
|
||||
</Layouts.app>
|
||||
|
||||
<script>
|
||||
const currentDataModelVersion = 7
|
||||
|
|
@ -107,11 +109,24 @@ function saveToIDB() {
|
|||
const tx = db
|
||||
.transaction("kinks", "readwrite")
|
||||
|
||||
let state = 0n;
|
||||
getAllSelections().forEach(([id, selection]) => {
|
||||
const shift = BigInt((Number(id) - 1) * 2);
|
||||
const selstate = BigInt(["fav", "yes", "may", "nah"].indexOf(selection));
|
||||
|
||||
state |= selstate << shift;
|
||||
|
||||
tx.objectStore("kinks").put({ id, selection })
|
||||
})
|
||||
|
||||
tx.oncomplete = () => contactBackend()
|
||||
if (params.get("flow") === "private") {
|
||||
tx.oncomplete = () => contactBackend()
|
||||
} else {
|
||||
tx.oncomplete = () => {
|
||||
const encoded = encodeState(state);
|
||||
window.location.href = `/flow/public?state=${encoded}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,10 +212,16 @@ request.onsuccess = (event) => {
|
|||
const kinks = event.target.result
|
||||
|
||||
kinks.forEach(({ id, selection }) => {
|
||||
document.querySelector(`input[id="${selection}-${id}"]`).checked = true
|
||||
const inputEl = document.querySelector(`input[id="${selection}-${id}"]`)
|
||||
if(inputEl) inputEl.checked = true
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
function encodeState(state) {
|
||||
// TODO: do better than decimal
|
||||
return state;
|
||||
}
|
||||
|
||||
document.addEventListener("touchstart", () => {}, true);
|
||||
</script>
|
||||
|
|
@ -1,34 +1,34 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<section class="mb-12">
|
||||
<h1 class="from-pink to-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-gradient-to-r font-extrabold font-display">the list</h1>
|
||||
<h1 class="from-ctp-pink to-ctp-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-linear-to-r font-extrabold font-display">the list</h1>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="mb-8 flex flex-col gap-1 px-2 text-md text-subtext1">
|
||||
<section class="mb-8 flex flex-col gap-1 px-2 text-md text-ctp-subtext1">
|
||||
<div>
|
||||
<div class="fa-solid fa-circle-heart text-sky pr-1"></div>
|
||||
<i class="fa-light fa-dash text-overlay0 pr-1"></i>
|
||||
<div class="fa-solid fa-circle-heart text-ctp-sky pr-1"></div>
|
||||
<i class="fa-light fa-dash text-ctp-overlay0 pr-1"></i>
|
||||
i want to do or try this
|
||||
</div>
|
||||
<div>
|
||||
<div class="fa-solid fa-circle-check text-green pr-1"></div>
|
||||
<i class="fa-light fa-dash text-overlay0 pr-1"></i>
|
||||
<div class="fa-solid fa-circle-check text-ctp-green pr-1"></div>
|
||||
<i class="fa-light fa-dash text-ctp-overlay0 pr-1"></i>
|
||||
i am okay with doing or trying this
|
||||
</div>
|
||||
<div>
|
||||
<div class="fa-solid fa-circle-exclamation text-yellow pr-1"></div>
|
||||
<i class="fa-light fa-dash text-overlay0 pr-1"></i>
|
||||
<div class="fa-solid fa-circle-exclamation text-ctp-yellow pr-1"></div>
|
||||
<i class="fa-light fa-dash text-ctp-overlay0 pr-1"></i>
|
||||
i need to talk about this first
|
||||
</div>
|
||||
<div>
|
||||
<div class="fa-solid fa-circle-xmark text-red pr-1"></div>
|
||||
<i class="fa-light fa-dash text-overlay0 pr-1"></i>
|
||||
<div class="fa-solid fa-circle-xmark text-ctp-red pr-1"></div>
|
||||
<i class="fa-light fa-dash text-ctp-overlay0 pr-1"></i>
|
||||
i don't want to do this – no exceptions
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="columns-1 md:columns-2 mt-8 gap-4">
|
||||
<%
|
||||
selected = KinkBio.Cache.read(@id)
|
||||
selected = KinkBioWeb.SessionController.load(@id)
|
||||
masterlist = KinkBio.Kinklist.get()
|
||||
%>
|
||||
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
<h2 class="text-sm text-overlay2 mb-1 uppercase"><%= category["name"] %></h2>
|
||||
<div class="relative -translate-x-1.5">
|
||||
<%= if category["spoiler"] do %>
|
||||
<div class="absolute w-full h-full flex flex-col justify-center items-center bg-base/95 backdrop-blur-sm cursor-pointer transition-all ease-in-out duration-150 z-10" onclick="this.classList.add('opacity-0'); setTimeout(() => this.classList.add('hidden'), 150)">
|
||||
<div class="absolute w-full h-full flex flex-col justify-center items-center bg-ctp-base/95 backdrop-blur-sm cursor-pointer transition-all ease-in-out duration-150 z-10" onclick="this.classList.add('opacity-0'); setTimeout(() => this.classList.add('hidden'), 150)">
|
||||
<i class="fa-duotone fa-circle-exclamation text-2xl"></i>
|
||||
<p class="text-center text-subtext1 max-w-[50%]">
|
||||
trigger warning
|
||||
|
|
@ -51,32 +51,32 @@
|
|||
</div>
|
||||
<% end %>
|
||||
<%= for kink <- category["kinks"] do %>
|
||||
<div class="flex flex-row justify-between items-center text-lg md:text-xl hover:bg-mantle px-1.5 rounded-lg">
|
||||
<div class="flex flex-row justify-between items-center text-lg md:text-xl hover:bg-ctp-mantle px-1.5 rounded-lg">
|
||||
<div class="tooltip cursor-help" data-text={kink["description"]}>
|
||||
<%= kink["name"] %>
|
||||
</div>
|
||||
<div class="flex space-x-2 w-fit fa-secondary-100">
|
||||
<%= case Map.get(selected.kinks, kink["name"]) do %>
|
||||
<%= case Map.get(selected.kinks, kink["id"]) do %>
|
||||
<% :fav -> %>
|
||||
<div class="block fa-solid fa-circle-heart text-sky"></div>
|
||||
<div class="block fa-solid fa-circle-heart text-ctp-sky"></div>
|
||||
<% :yes -> %>
|
||||
<div class="block fa-solid fa-circle-check text-green"></div>
|
||||
<div class="block fa-solid fa-circle-check text-ctp-green"></div>
|
||||
<% :may -> %>
|
||||
<div class="block fa-solid fa-circle-exclamation text-yellow"></div>
|
||||
<div class="block fa-solid fa-circle-exclamation text-ctp-yellow"></div>
|
||||
<% :nah -> %>
|
||||
<div class="block fa-solid fa-circle-xmark text-red"></div>
|
||||
<div class="block fa-solid fa-circle-xmark text-ctp-red"></div>
|
||||
<% {:fav, :fav} -> %>
|
||||
<i class="block fa-kit-duotone fa-circle-heart-circle-heart fa-sky"></i>
|
||||
<i class="block fa-kit-duotone fa-circle-heart-circle-heart fa-primary-ctp-sky fa-secondary-ctp-sky"></i>
|
||||
<% {:fav, :yes} -> %>
|
||||
<i class="block fa-kit-duotone fa-circle-heart-circle-check fa-primary-green fa-secondary-sky"></i>
|
||||
<i class="block fa-kit-duotone fa-circle-heart-circle-check fa-primary-ctp-green fa-secondary-ctp-sky"></i>
|
||||
<% {:fav, :may} -> %>
|
||||
<i class="block fa-kit-duotone fa-circle-heart-circle-exclamation fa-primary-yellow fa-secondary-sky"></i>
|
||||
<i class="block fa-kit-duotone fa-circle-heart-circle-exclamation fa-primary-ctp-yellow fa-secondary-ctp-sky"></i>
|
||||
<% {:yes, :yes} -> %>
|
||||
<i class="block fa-kit-duotone fa-circle-check-circle-check fa-green"></i>
|
||||
<i class="block fa-kit-duotone fa-circle-check-circle-check fa-primary-ctp-green fa-secondary-ctp-green"></i>
|
||||
<% {:yes, :may} -> %>
|
||||
<i class="block fa-kit-duotone fa-circle-check-circle-exclamation fa-primary-yellow fa-secondary-green"></i>
|
||||
<i class="block fa-kit-duotone fa-circle-check-circle-exclamation fa-primary-ctp-yellow fa-secondary-ctp-green"></i>
|
||||
<% {:may, :may} -> %>
|
||||
<i class="block fa-kit-duotone fa-circle-exclamation-circle-exclamation fa-yellow"></i>
|
||||
<i class="block fa-kit-duotone fa-circle-exclamation-circle-exclamation fa-primary-ctp-yellow fa-secondary-ctp-yellow"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -86,14 +86,4 @@
|
|||
<% end %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.tooltip-wrapper:hover .tooltip-content {
|
||||
display: block;
|
||||
}
|
||||
.tooltip-wrapper .tooltip-content {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</Layouts.app>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
defmodule KinkBioWeb.SessionController do
|
||||
import Bitwise
|
||||
use KinkBioWeb, :controller
|
||||
|
||||
alias KinkBio.Cache
|
||||
|
|
@ -80,6 +81,41 @@ defmodule KinkBioWeb.SessionController do
|
|||
end
|
||||
end
|
||||
|
||||
def load(id) do
|
||||
# TODO: state paramter can be encoded better than decimal
|
||||
case Integer.parse(id) do
|
||||
{state, _} ->
|
||||
kinks =
|
||||
KinkBio.Kinklist.get()
|
||||
|> Enum.flat_map(fn c -> c["kinks"] end)
|
||||
|> Enum.map(fn kink ->
|
||||
shift = (kink["id"] - 1) * 2
|
||||
sel = state >>> shift &&& 0b11
|
||||
|
||||
sel =
|
||||
case sel do
|
||||
0 -> :fav
|
||||
1 -> :yes
|
||||
2 -> :may
|
||||
3 -> :nah
|
||||
end
|
||||
|
||||
{kink["id"], sel}
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
%{
|
||||
kinks: kinks,
|
||||
flow: :public,
|
||||
secret: "",
|
||||
stage: :published
|
||||
}
|
||||
|
||||
:error ->
|
||||
Cache.read(id)
|
||||
end
|
||||
end
|
||||
|
||||
defp calculate_kink_mutuality(_k, a, b), do: calculate_kink_mutuality(a, b)
|
||||
# if someone says no, it's no
|
||||
defp calculate_kink_mutuality(:nah, _b), do: :nah
|
||||
|
|
|
|||
|
|
@ -7,20 +7,25 @@ defmodule KinkBioWeb.Endpoint do
|
|||
@session_options [
|
||||
store: :cookie,
|
||||
key: "_kink_bio_key",
|
||||
signing_salt: "CPUtNyBw"
|
||||
signing_salt: "CPUtNyBw",
|
||||
same_site: "Lax"
|
||||
]
|
||||
|
||||
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
|
||||
socket "/live", Phoenix.LiveView.Socket,
|
||||
websocket: [connect_info: [session: @session_options]],
|
||||
longpoll: [connect_info: [session: @session_options]]
|
||||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
#
|
||||
# You should set gzip to true if you are running phx.digest
|
||||
# when deploying your static files in production.
|
||||
# When code reloading is disabled (e.g., in production),
|
||||
# the `gzip` option is enabled to serve compressed
|
||||
# static files generated by running `phx.digest`.
|
||||
plug Plug.Static,
|
||||
at: "/",
|
||||
from: :kink_bio,
|
||||
gzip: false,
|
||||
only: ~w(assets fonts images favicon.ico robots.txt)
|
||||
gzip: not code_reloading?,
|
||||
only: KinkBioWeb.static_paths(),
|
||||
raise_on_missing_only: code_reloading?
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
|
|
@ -36,7 +41,6 @@ defmodule KinkBioWeb.Endpoint do
|
|||
cookie_key: "request_logger"
|
||||
|
||||
plug Plug.RequestId
|
||||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
|
|
|
|||
|
|
@ -26,8 +26,4 @@ defmodule KinkBioWeb.ConsentLive do
|
|||
def handle_info(_payload, socket) do
|
||||
{:noreply, redirect(socket, to: "/view/#{socket.assigns.session}")}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
KinkBioWeb.PageView.render("consent.html", assigns)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<section class="mb-12">
|
||||
<h1 class="from-pink to-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-gradient-to-r font-extrabold font-display">consent comes first</h1>
|
||||
<h1 class="from-ctp-pink to-ctp-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-linear-to-r font-extrabold font-display">consent comes first</h1>
|
||||
</section>
|
||||
|
||||
<section class="text-subtext0 max-w-screen-md mb-8">
|
||||
<p class="text-2xl text-text mb-1">
|
||||
You and your partner have <span class="text-mauve"><%= @common_count %> <%= if @common_count == 1, do: "kink", else: "kinks" %></span> in common.
|
||||
<section class="text-ctp-subtext0 max-w-3xl mb-8">
|
||||
<p class="text-2xl text-ctp-text mb-1">
|
||||
You and your partner have <span class="text-ctp-mauve"><%= @common_count %> <%= if @common_count == 1, do: "kink", else: "kinks" %></span> in common.
|
||||
</p>
|
||||
<p>
|
||||
Make sure this number doesn't seem abnormally high.
|
||||
|
|
@ -14,37 +15,37 @@
|
|||
</p>
|
||||
</section>
|
||||
|
||||
<section class="text-subtext0 max-w-screen-md mb-8">
|
||||
<section class="text-ctp-subtext0 max-w-3xl mb-8">
|
||||
<p>
|
||||
In common means that neither of you are opposed <i class="fa-solid fa-circle-xmark text-red text-sm"></i> to that kink.
|
||||
In common means that neither of you are opposed <i class="fa-solid fa-circle-xmark text-ctp-red text-sm"></i> to that kink.
|
||||
</p>
|
||||
<p>
|
||||
Here are some example outcomes:
|
||||
</p>
|
||||
<div class="columns-1 sm:columns-3 mt-2 text-lg *:break-inside-avoid *:mb-2 fa-secondary-100">
|
||||
<div>
|
||||
<i class="fa-solid fa-circle-heart text-sky"></i>
|
||||
<i class="fa-solid fa-circle-xmark text-red"></i>
|
||||
<i class="fa-solid fa-circle-heart text-ctp-sky"></i>
|
||||
<i class="fa-solid fa-circle-xmark text-ctp-red"></i>
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
<i class="fa-solid fa-circle-xmark text-red"></i>
|
||||
<i class="fa-solid fa-circle-xmark text-ctp-red"></i>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fa-solid fa-circle-check text-green"></i>
|
||||
<i class="fa-solid fa-circle-heart text-sky"></i>
|
||||
<i class="fa-solid fa-circle-check text-ctp-green"></i>
|
||||
<i class="fa-solid fa-circle-heart text-ctp-sky"></i>
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
<i class="fa-kit-duotone fa-circle-heart-circle-check fa-primary-green fa-secondary-sky"></i>
|
||||
<i class="fa-kit-duotone fa-circle-heart-circle-check fa-primary-ctp-green fa-secondary-ctp-sky"></i>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fa-solid fa-circle-exclamation text-yellow"></i>
|
||||
<i class="fa-solid fa-circle-exclamation text-yellow"></i>
|
||||
<i class="fa-solid fa-circle-exclamation text-ctp-yellow"></i>
|
||||
<i class="fa-solid fa-circle-exclamation text-ctp-yellow"></i>
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
<i class="fa-kit-duotone fa-circle-exclamation-circle-exclamation fa-yellow"></i>
|
||||
<i class="fa-kit-duotone fa-circle-exclamation-circle-exclamation fa-primary-ctp-yellow fa-secondary-ctp-yellow"></i>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="max-w-screen-md">
|
||||
<p class="text-subtext0 mb-2">
|
||||
<section class="max-w-3xl">
|
||||
<p class="text-ctp-subtext0 mb-2">
|
||||
Whenever you're ready, click the button below to share your common kinks with your partner.
|
||||
</p>
|
||||
|
||||
|
|
@ -58,9 +59,9 @@
|
|||
el.disabled = true
|
||||
el.innerHTML = "Waiting for partner..."
|
||||
el.classList.add("cursor-not-allowed")
|
||||
el.classList.remove("hover:bg-pink")
|
||||
el.classList.add("bg-mauve/40")
|
||||
el.classList.remove("bg-mauve")
|
||||
el.classList.remove("hover:bg-ctp-pink")
|
||||
el.classList.add("bg-ctp-mauve/40")
|
||||
el.classList.remove("bg-ctp-mauve")
|
||||
|
||||
fetch(`/api/sessions/${session}/consent`, {
|
||||
headers: {
|
||||
|
|
@ -75,7 +76,8 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<button class="bg-mauve hover:bg-pink transition rounded-md px-4 py-2 text-mantle" onclick="consent(this)">
|
||||
<button class="bg-ctp-mauve hover:bg-ctp-pink transition rounded-md px-4 py-2 text-ctp-mantle cursor-pointer" onclick="consent(this)">
|
||||
Consent & Continue <i class="fa-solid fa-arrow-right ml-2"></i>
|
||||
</button>
|
||||
</section>
|
||||
</Layouts.app>
|
||||
|
|
@ -12,10 +12,6 @@ defmodule KinkBioWeb.WaitingLive do
|
|||
{:ok, assign(socket, state: entry.stage, session: session)}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
KinkBioWeb.PageView.render("waiting.html", assigns)
|
||||
end
|
||||
|
||||
def handle_info(_payload, socket) do
|
||||
{:noreply, redirect(socket, to: "/consent?session=#{socket.assigns.session}")}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<section class="mb-12">
|
||||
<h1 class="from-pink to-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-gradient-to-r font-extrabold font-display">sharing is caring</h1>
|
||||
<h1 class="from-ctp-pink to-ctp-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-linear-to-r font-extrabold font-display">sharing is caring</h1>
|
||||
</section>
|
||||
|
||||
<section class="mb-12 text-2xl flex flex-col md:flex-row">
|
||||
<button min-w-[50%] class="border border-solid border-pink bg-surface0 text-text font-bold py-2 px-4 max-md:rounded-t-lg md:rounded-l-lg transition-all ease-in-out duration-150" disabled onclick="view()" id="link">
|
||||
<button min-w-[50%] class="border border-solid border-ctp-pink bg-ctp-surface0 text-ctp-text font-bold py-2 px-4 max-md:rounded-t-lg md:rounded-l-lg transition-all ease-in-out duration-150" disabled id="link">
|
||||
https://kink.bio/flow/join
|
||||
</button>
|
||||
<button min-w-[50%] class="bg-gradient-to-l from-red to-pink p-1 transition-all ease-in-out duration-500 bg-size-200 bg-pos-0 hover:bg-pos-100 text-crust font-bold py-2 px-4 md:rounded-r-lg max-md:rounded-b-lg md:w-40 flex items-center justify-center" onclick="copy(this)">
|
||||
<button min-w-[50%] class="bg-linear-to-l from-ctp-red to-ctp-pink p-1 transition-all ease-in-out duration-500 bg-size-200 bg-pos-0 hover:bg-pos-100 text-ctp-crust font-bold py-2 px-4 md:rounded-r-lg max-md:rounded-b-lg md:w-40 flex items-center justify-center cursor-pointer" onclick="copy(this)">
|
||||
Copy
|
||||
</button>
|
||||
</section>
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
</script>
|
||||
<% end %>
|
||||
</section>
|
||||
</Layouts.app>
|
||||
|
||||
<script>
|
||||
const link_el = document.getElementById("link");
|
||||
|
|
@ -33,20 +35,16 @@ const url = `${location.origin}/flow/join?session=${session}`;
|
|||
|
||||
let state = false;
|
||||
|
||||
function view(el) {
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
async function copy(el) {
|
||||
el.innerHTML = "Copied"
|
||||
el.classList.remove("duration-500");
|
||||
el.classList.remove("transition-500");
|
||||
el.classList.add("bg-green");
|
||||
el.classList.remove("bg-gradient-to-l");
|
||||
el.classList.add("bg-ctp-green");
|
||||
el.classList.remove("bg-linear-to-l");
|
||||
|
||||
link_el.classList.remove("border-surface1");
|
||||
link_el.classList.remove("border-pink");
|
||||
link_el.classList.add("border-green");
|
||||
link_el.classList.remove("border-ctp-surface1");
|
||||
link_el.classList.remove("border-ctp-pink");
|
||||
link_el.classList.add("border-ctp-green");
|
||||
|
||||
navigator.clipboard.writeText(url);
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ defmodule KinkBioWeb.Router do
|
|||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
plug :fetch_live_flash
|
||||
plug :put_root_layout, {KinkBioWeb.LayoutView, :root}
|
||||
plug :put_root_layout, html: {KinkBioWeb.Layouts, :root}
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
end
|
||||
|
|
@ -42,20 +42,20 @@ defmodule KinkBioWeb.Router do
|
|||
post "/sessions/:id/consent", SessionController, :consent
|
||||
end
|
||||
|
||||
# Enables LiveDashboard only for development
|
||||
#
|
||||
# If you want to use the LiveDashboard in production, you should put
|
||||
# it behind authentication and allow only admins to access it.
|
||||
# If your application does not have an admins-only section yet,
|
||||
# you can use Plug.BasicAuth to set up some basic authentication
|
||||
# as long as you are also using SSL (which you should anyway).
|
||||
if Mix.env() in [:dev, :test] do
|
||||
# Enable LiveDashboard and Swoosh mailbox preview in development
|
||||
if Application.compile_env(:kink_bio, :dev_routes) do
|
||||
# If you want to use the LiveDashboard in production, you should put
|
||||
# it behind authentication and allow only admins to access it.
|
||||
# If your application does not have an admins-only section yet,
|
||||
# you can use Plug.BasicAuth to set up some basic authentication
|
||||
# as long as you are also using SSL (which you should anyway).
|
||||
import Phoenix.LiveDashboard.Router
|
||||
|
||||
scope "/" do
|
||||
scope "/dev" do
|
||||
pipe_through :browser
|
||||
|
||||
live_dashboard "/dashboard", metrics: KinkBioWeb.Telemetry
|
||||
live_dashboard "/dashboard"
|
||||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
defmodule KinkBioWeb.Telemetry do
|
||||
use Supervisor
|
||||
import Telemetry.Metrics
|
||||
|
||||
def start_link(arg) do
|
||||
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_arg) do
|
||||
children = [
|
||||
# Telemetry poller will execute the given period measurements
|
||||
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
|
||||
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
|
||||
# Add reporters as children of your supervision tree.
|
||||
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
def metrics do
|
||||
[
|
||||
# Phoenix Metrics
|
||||
summary("phoenix.endpoint.stop.duration",
|
||||
unit: {:native, :millisecond}
|
||||
),
|
||||
summary("phoenix.router_dispatch.stop.duration",
|
||||
tags: [:route],
|
||||
unit: {:native, :millisecond}
|
||||
),
|
||||
|
||||
# Database Metrics
|
||||
summary("kink_bio.repo.query.total_time",
|
||||
unit: {:native, :millisecond},
|
||||
description: "The sum of the other measurements"
|
||||
),
|
||||
summary("kink_bio.repo.query.decode_time",
|
||||
unit: {:native, :millisecond},
|
||||
description: "The time spent decoding the data received from the database"
|
||||
),
|
||||
summary("kink_bio.repo.query.query_time",
|
||||
unit: {:native, :millisecond},
|
||||
description: "The time spent executing the query"
|
||||
),
|
||||
summary("kink_bio.repo.query.queue_time",
|
||||
unit: {:native, :millisecond},
|
||||
description: "The time spent waiting for a database connection"
|
||||
),
|
||||
summary("kink_bio.repo.query.idle_time",
|
||||
unit: {:native, :millisecond},
|
||||
description:
|
||||
"The time the connection spent waiting before being checked out for the query"
|
||||
),
|
||||
|
||||
# VM Metrics
|
||||
summary("vm.memory.total", unit: {:byte, :kilobyte}),
|
||||
summary("vm.total_run_queue_lengths.total"),
|
||||
summary("vm.total_run_queue_lengths.cpu"),
|
||||
summary("vm.total_run_queue_lengths.io")
|
||||
]
|
||||
end
|
||||
|
||||
defp periodic_measurements do
|
||||
[
|
||||
# A module, function and arguments to be invoked periodically.
|
||||
# This function must call :telemetry.execute/3 and a metric must be added above.
|
||||
# {KinkBioWeb, :count_users, []}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<main class="h-full mx-auto max-w-5xl px-4">
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<main class="h-full mx-auto max-w-5xl px-4">
|
||||
<p class="alert alert-info" role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
|
||||
|
||||
<p class="alert alert-danger" role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
|
||||
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<section class="mb-12">
|
||||
<h1 class="from-pink to-mauve text-5xl md:text-7xl text-transparent bg-clip-text pb-3 -mb-3 bg-gradient-to-r font-extrabold font-display">publish your list</h1>
|
||||
</section>
|
||||
|
||||
<section class="mb-12 text-2xl flex flex-col md:flex-row">
|
||||
<button min-w-[50%] class="border border-solid border-surface1 bg-surface1 text-subtext0 font-bold py-2 px-4 max-md:rounded-t-lg md:rounded-l-lg transition-all ease-in-out duration-150" disabled onclick="view()" id="link">
|
||||
https://kink.bio/view
|
||||
</button>
|
||||
<button min-w-[50%] class="bg-gradient-to-l from-red to-pink p-1 transition-all ease-in-out duration-500 bg-size-200 bg-pos-0 hover:bg-pos-100 text-crust font-bold py-2 px-4 md:rounded-r-lg max-md:rounded-b-lg md:w-40" onclick="publish(this)">
|
||||
Publish
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="text-subtext0 italic">
|
||||
This link will be valid for 24 hours. After that, it will be deleted.
|
||||
</section>
|
||||
|
||||
|
||||
<script>
|
||||
const link_el = document.getElementById("link");
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const session = params.get("session");
|
||||
const url = `${location.origin}/view/${session}`;
|
||||
|
||||
let state = false;
|
||||
|
||||
function view(el) {
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
async function publish(el) {
|
||||
if (!state) {
|
||||
await fetch(`/api/sessions/${session}/publish`, {
|
||||
headers: {
|
||||
"accept": "application/json",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
secret: sessionStorage.getItem("kb-secret")
|
||||
})
|
||||
})
|
||||
|
||||
link_el.classList.remove("bg-surface1");
|
||||
link_el.classList.add("bg-surface0");
|
||||
|
||||
link_el.classList.remove("text-subtext0");
|
||||
link_el.classList.add("text-text");
|
||||
|
||||
link_el.classList.remove("border-surface1");
|
||||
link_el.classList.add("border-pink");
|
||||
|
||||
link_el.classList.add("hover:bg-gradient-to-l");
|
||||
link_el.classList.add("hover:from-pink");
|
||||
link_el.classList.add("hover:to-mauve");
|
||||
link_el.classList.add("hover:text-surface0");
|
||||
|
||||
link_el.disabled = false;
|
||||
|
||||
el.innerHTML = "Copy"
|
||||
state = true;
|
||||
} else {
|
||||
el.innerHTML = "Copied"
|
||||
el.classList.remove("duration-500");
|
||||
el.classList.remove("transition-500");
|
||||
el.classList.add("bg-green");
|
||||
el.classList.remove("bg-gradient-to-l");
|
||||
|
||||
link_el.classList.remove("border-surface1");
|
||||
link_el.classList.remove("border-pink");
|
||||
link_el.classList.add("border-green");
|
||||
|
||||
link_el.classList.remove("hover:from-pink");
|
||||
link_el.classList.remove("hover:to-mauve");
|
||||
link_el.classList.add("hover:from-green");
|
||||
link_el.classList.add("hover:to-sapphire");
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(url);
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<section class="mb-12">
|
||||
<h1 class="from-pink to-mauve text-5xl md:text-7xl text-transparent bg-clip-text bg-gradient-to-r font-extrabold font-display">create a link</h1>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 h-full mt-8 gap-4 select-none">
|
||||
<%= link to: "/select?flow=private" do %>
|
||||
<button type="button" class="rounded-lg bg-gradient-to-tl from-sky via-mauve to-pink p-1 transition-all ease-in-out duration-500 bg-size-200 bg-pos-0 hover:bg-pos-100 aspect-square">
|
||||
<div class="bg-base hover:bg-mantle transition-all ease-in-out duration-500 h-full w-full rounded-md">
|
||||
<div class="flex flex-col justify-center items-center h-full w-full p-12 md:p-24 m-auto gap-2">
|
||||
<i class="fa-duotone fa-key text-5xl"></i>
|
||||
<h3 class="mt-8 text-4xl font-bold text-text">private</h3>
|
||||
<p class="mt-1 text-lg text-subtext1">share kinks with a trusted person</p>
|
||||
<p class="text-sm text-subtext0 text-center">this will generate a one-time link for you to send to your trustee – only kinks you two have in common will be shown</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<% end %>
|
||||
<%= link to: "/select?flow=public" do %>
|
||||
<button type="button" class="rounded-lg bg-gradient-to-tl from-green via-sky to-mauve p-1 transition-all ease-in-out duration-500 bg-size-200 bg-pos-0 hover:bg-pos-100 aspect-square">
|
||||
<div class="bg-base hover:bg-mantle transition-all ease-in-out duration-500 h-full w-full rounded-md">
|
||||
<div class="flex flex-col justify-center items-center h-full w-full p-12 md:p-24 m-auto gap-2">
|
||||
<i class="fa-duotone fa-earth-africa text-5xl"></i>
|
||||
<h3 class="mt-8 text-4xl font-bold text-text">public</h3>
|
||||
<p class="mt-1 text-lg text-subtext1">generate a publicly accessible link</p>
|
||||
<p class="text-sm text-subtext0 text-center">this will generate a public link to your kink list – it won't be visible without the link, but anyone with the link will be able to view it</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<p class="text-subtext0">all links automatically expire after 24h</p>
|
||||
</section>
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
defmodule KinkBioWeb.ErrorHelpers do
|
||||
@moduledoc """
|
||||
Conveniences for translating and building error messages.
|
||||
"""
|
||||
|
||||
use Phoenix.HTML
|
||||
|
||||
@doc """
|
||||
Generates tag for inlined form input errors.
|
||||
"""
|
||||
def error_tag(form, field) do
|
||||
Enum.map(Keyword.get_values(form.errors, field), fn error ->
|
||||
content_tag(:span, translate_error(error),
|
||||
class: "invalid-feedback",
|
||||
phx_feedback_for: input_name(form, field)
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Translates an error message.
|
||||
"""
|
||||
def translate_error({msg, opts}) do
|
||||
# Because the error messages we show in our forms and APIs
|
||||
# are defined inside Ecto, we need to translate them dynamically.
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
defmodule KinkBioWeb.ErrorView do
|
||||
use KinkBioWeb, :view
|
||||
|
||||
# If you want to customize a particular status code
|
||||
# for a certain format, you may uncomment below.
|
||||
# def render("500.html", _assigns) do
|
||||
# "Internal Server Error"
|
||||
# end
|
||||
|
||||
# By default, Phoenix returns the status message from
|
||||
# the template name. For example, "404.html" becomes
|
||||
# "Not Found".
|
||||
def template_not_found(template, _assigns) do
|
||||
Phoenix.Controller.status_message_from_template(template)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
defmodule KinkBioWeb.LayoutView do
|
||||
use KinkBioWeb, :view
|
||||
|
||||
# Phoenix LiveDashboard is available only in development by default,
|
||||
# so we instruct Elixir to not warn if the dashboard route is missing.
|
||||
@compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}}
|
||||
end
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
defmodule KinkBioWeb.PageView do
|
||||
use KinkBioWeb, :view
|
||||
end
|
||||
70
mix.exs
70
mix.exs
|
|
@ -5,12 +5,13 @@ defmodule KinkBio.MixProject do
|
|||
[
|
||||
app: :kink_bio,
|
||||
version: "0.2.0",
|
||||
elixir: "~> 1.12",
|
||||
elixir: "~> 1.15",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: Mix.compilers(),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
aliases: aliases(),
|
||||
deps: deps()
|
||||
deps: deps(),
|
||||
compilers: [:phoenix_live_view] ++ Mix.compilers(),
|
||||
listeners: [Phoenix.CodeReloader]
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -24,6 +25,12 @@ defmodule KinkBio.MixProject do
|
|||
]
|
||||
end
|
||||
|
||||
def cli do
|
||||
[
|
||||
preferred_envs: [precommit: :test]
|
||||
]
|
||||
end
|
||||
|
||||
# Specifies which paths to compile per environment.
|
||||
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
|
@ -32,25 +39,37 @@ defmodule KinkBio.MixProject do
|
|||
#
|
||||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.6.15"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:ecto_sql, "~> 3.6"},
|
||||
phoenix = [
|
||||
{:phoenix, "~> 1.8.3"},
|
||||
{:phoenix_ecto, "~> 4.5"},
|
||||
{:ecto_sql, "~> 3.13"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:phoenix_html, "~> 3.0"},
|
||||
{:phoenix_html, "~> 4.1"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_live_view, "~> 0.17.5"},
|
||||
{:floki, ">= 0.30.0", only: :test},
|
||||
{:phoenix_live_dashboard, "~> 0.6"},
|
||||
{:esbuild, "~> 0.4", runtime: Mix.env() == :dev},
|
||||
{:telemetry_metrics, "~> 0.6"},
|
||||
{:telemetry_poller, "~> 1.0"},
|
||||
{:phoenix_live_view, "~> 1.1.0"},
|
||||
{:lazy_html, ">= 0.1.0", only: :test},
|
||||
{:phoenix_live_dashboard, "~> 0.8.3"},
|
||||
# {:phoenix_view, "~> 2.0"},
|
||||
{:esbuild, "~> 0.10", runtime: Mix.env() == :dev},
|
||||
{:tailwind, "~> 0.4", runtime: Mix.env() == :dev},
|
||||
{:heroicons,
|
||||
github: "tailwindlabs/heroicons",
|
||||
tag: "v2.2.0",
|
||||
sparse: "optimized",
|
||||
app: false,
|
||||
compile: false,
|
||||
depth: 1},
|
||||
{:swoosh, "~> 1.16"},
|
||||
{:req, "~> 0.5"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:tailwind, "~> 0.2.2", runtime: Mix.env() == :dev},
|
||||
{:dns_cluster, "~> 0.2.0"},
|
||||
{:bandit, "~> 1.5"}
|
||||
]
|
||||
|
||||
[
|
||||
{:yaml_elixir, "~> 2.9"},
|
||||
{:zbase32, "~> 2.0"}
|
||||
]
|
||||
] ++ phoenix
|
||||
end
|
||||
|
||||
# Aliases are shortcuts or tasks specific to the current project.
|
||||
|
|
@ -61,11 +80,24 @@ defmodule KinkBio.MixProject do
|
|||
# See the documentation for `Mix` for more info on aliases.
|
||||
defp aliases do
|
||||
[
|
||||
setup: ["deps.get", "ecto.setup"],
|
||||
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
|
||||
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
||||
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
|
||||
"assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
|
||||
"assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
|
||||
"assets.build": [
|
||||
"compile",
|
||||
"cmd --cd assets npm ci",
|
||||
"tailwind kink_bio",
|
||||
"esbuild kink_bio"
|
||||
],
|
||||
"assets.deploy": [
|
||||
"cmd --cd assets npm ci",
|
||||
"tailwind kink_bio --minify",
|
||||
"esbuild kink_bio --minify",
|
||||
"phx.digest"
|
||||
],
|
||||
precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
57
mix.lock
57
mix.lock
|
|
@ -1,36 +1,55 @@
|
|||
%{
|
||||
"castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
|
||||
"cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
|
||||
"bandit": {:hex, :bandit, "1.9.0", "6dc1ff2c30948dfecf32db574cc3447c7b9d70e0b61140098df3818870b01b76", [: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.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "2538aaa1663b40ca9cbd8ca1f8a540cb49e5baf34c6ffef068369cc45f9146f2"},
|
||||
"castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||
"cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"},
|
||||
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
||||
"cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"},
|
||||
"db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
|
||||
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.13.3", "81f7067dd1951081888529002dbc71f54e5e891b69c60195040ea44697e1104a", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5751caea36c8f5dd0d1de6f37eceffea19d10bd53f20e5bbe31c45f2efc8944a"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
|
||||
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
|
||||
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
|
||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||
"phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"},
|
||||
"lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [: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", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [: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", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.17.14", "5ec615d4d61bf9d4755f158bd6c80372b715533fe6d6219e12d74fb5eedbeac1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "afeb6ba43ce329a6f7fc1c9acdfc6d3039995345f025febb7f409a92f6faebd3"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.19", "c95e9acbc374fb796ee3e24bfecc8213123c74d9f9e45667ca40bb0a4d242953", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d5ad357d6b21562a5b431f0ad09dfe76db9ce5648c6949f1aac334c8c4455d32"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||
"plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
|
||||
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
|
||||
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.5", "261f21b67aea8162239b2d6d3b4c31efde4daa22a20d80b19c2c0f21b34b270e", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20884bf58a90ff5a5663420f5d2c368e9e15ed1ad5e911daf0916ea3c57f77ac"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"},
|
||||
"secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"},
|
||||
"tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},
|
||||
"swoosh": {:hex, :swoosh, "1.19.9", "4eb2c471b8cf06adbdcaa1d57a0ad53c0ed9348ce8586a06cc491f9f0dbcb553", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "516898263a64925c31723c56bc7999a26e97b04e869707f681f4c9bca7ee1688"},
|
||||
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
|
||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
|
||||
"zbase32": {:hex, :zbase32, "2.0.0", "5a61d5ee8f39092d4a243da2a42b5b4339ef226d9b182603f63d5a3f16d192ee", [:mix], [], "hexpm", "798f81895658f9773e1dcf30ba3c118547f482502c5e1e19e72752f9a6f23e44"},
|
||||
|
|
|
|||
|
|
@ -1,471 +1,669 @@
|
|||
# Hi :) Please ensure that these kinks keep unique IDs.
|
||||
# When adding a new kink, give it a new ID.
|
||||
# When removing a kink, retire that ID.
|
||||
|
||||
- name: clothing / accessories
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: clothed sex
|
||||
description: sexual interaction where at least one partner is not undressed
|
||||
id: 0
|
||||
- name: strip tease
|
||||
description: dramatic removal of clothing, typically as a form of seduction
|
||||
id: 1
|
||||
- name: heels
|
||||
description: high heeled shoes; sometimes as part of pain play
|
||||
id: 2
|
||||
- name: latex
|
||||
description: latex and/or rubber material worn as clothing
|
||||
id: 3
|
||||
- name: leather
|
||||
description: often in a bondage situation or worn as a harness
|
||||
id: 4
|
||||
- name: lingerie
|
||||
description: alluring underwear, sleepwear, or light robes; often semi-transparent
|
||||
id: 5
|
||||
- name: masks
|
||||
description: commonly leather/latex, surgical, gas masks; often to conceal wearer's identity
|
||||
id: 6
|
||||
- name: stockings
|
||||
description: close-fitting garments covering the leg from the foot up, often to the knee or thigh
|
||||
id: 7
|
||||
- name: uniform / costume
|
||||
description: often as a part of a role, e.g. a maid dress for a servant
|
||||
id: 8
|
||||
- name: intimate piercings
|
||||
description: piercings located on the nipples or genitals, sometimes used with chastity devices
|
||||
id: 9
|
||||
- name: underwear
|
||||
description: as a sex toy (e.g. a makeshift gag), or for their scent or feel
|
||||
id: 10
|
||||
- name: diapers
|
||||
description: often worn by a little, or as part of bathroom denial
|
||||
id: 11
|
||||
|
||||
- name: behavior
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: competition
|
||||
description: multiple partners competing to please one the best
|
||||
id: 12
|
||||
- name: power imbalance
|
||||
description: having controlling and controlled partners, sometimes as part of coercion
|
||||
id: 13
|
||||
- name: conditioning
|
||||
description: having favorable traits/behaviors reinforced through reward or punishment
|
||||
id: 14
|
||||
- name: clicker training
|
||||
description: conditioning using a dog clicker as a distinct marker of favorable behavior
|
||||
id: 15
|
||||
- name: uppity
|
||||
description: the submissive partner being combative/disobedient, often playfully
|
||||
id: 16
|
||||
- name: cross-dressing
|
||||
description: wearing clothes associated with the gender opposite of one's identity
|
||||
id: 17
|
||||
- name: feminization
|
||||
description: having traditionally female-associated clothes/behaviors/mannerisms imposed onto oneself
|
||||
id: 18
|
||||
- name: exhibitionism
|
||||
description: being watched by others (often strangers)
|
||||
id: 19
|
||||
- name: voyeurism
|
||||
description: watching others (often strangers)
|
||||
id: 20
|
||||
- name: photography / videotaping
|
||||
description: recording a scene, often for humiliation, exhibitionism, or blackmail
|
||||
id: 21
|
||||
- name: food play
|
||||
description: incorporating food into sexual context, sometimes eaten off one's body
|
||||
id: 22
|
||||
- name: force feeding
|
||||
description: being forced to consume [large amounts of] food, often for involuntary weight gain
|
||||
id: 23
|
||||
- name: dirty talking
|
||||
description: speaking in a typically explicit manner to arouse or provoke
|
||||
id: 24
|
||||
|
||||
- name: headspaces / roles
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: caretaker + little
|
||||
description: a caring/parental role and a dependent/childlike role
|
||||
id: 25
|
||||
- name: pet play
|
||||
description: a domesticated animal, or humiliation by being treated as one
|
||||
id: 26
|
||||
- name: worship
|
||||
description: a deity and an acolyte, often involving physical worship or unquestioning submission
|
||||
id: 27
|
||||
- name: primal
|
||||
description: instinct and raw emotion, often involving growling, biting, scratching
|
||||
id: 28
|
||||
- name: slavery
|
||||
description: role involving involuntary servitude, often with that being taken advantage of
|
||||
id: 29
|
||||
- name: domestic servitude
|
||||
description: taking care of chores, often as a butler, waiter, chauffeur, or housekeeper
|
||||
id: 30
|
||||
- name: objectification
|
||||
description: an inanimate object, often furniture; or being treated as equal in value to one
|
||||
id: 31
|
||||
- name: breeding
|
||||
description: sexual intercourse to [pretend to] induce pregnancy or produce offspring
|
||||
id: 32
|
||||
- name: mind control
|
||||
description: inducing thoughts or behaviors into a partner, e.g. through hypnosis
|
||||
id: 33
|
||||
- name: memory play
|
||||
description: mind control where the recipient is made to forget, and subsequently unaware
|
||||
id: 34
|
||||
|
||||
- name: denial
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: teasing
|
||||
description: physical or verbal means of arousal, often to achieve frustration or desperation
|
||||
id: 35
|
||||
- name: orgasm control
|
||||
description: either by physical means or by command; often paired with teasing or edging
|
||||
id: 36
|
||||
- name: edging
|
||||
description: keeping arousal just short of climax, often to cause desperation
|
||||
id: 37
|
||||
- name: denial (temporary)
|
||||
description: typically as a form of punishment, or to build arousal
|
||||
id: 38
|
||||
- name: denial (long-term)
|
||||
description: typically as a form of punishment, as part of conditioning, or to build arousal
|
||||
id: 39
|
||||
- name: chastity
|
||||
description: abstinence, often forced; usually involving a device that prevents genital stimulation
|
||||
id: 40
|
||||
- name: sexual frustration
|
||||
description: exceptional desperation or arousal, often with the goal of disinhibition
|
||||
id: 41
|
||||
- name: cuckoldry
|
||||
description: being intentionally and knowingly uninvolved during others' sexual acts
|
||||
id: 42
|
||||
- name: interaction restrictions
|
||||
description: commands limiting interaction with one's body, others, or the environment
|
||||
id: 43
|
||||
- name: speech restrictions
|
||||
description: commands limiting speech or other noises; e.g. to begging, animal noises, or silence
|
||||
id: 44
|
||||
|
||||
- name: penetration
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: penetration
|
||||
description: insertion of an object (often a penis or dildo) into an orifice
|
||||
id: 45
|
||||
- name: multiple penetration
|
||||
description: penetration involving two or more objects, often into the same orifice
|
||||
id: 46
|
||||
- name: gangbang
|
||||
description: multiple partners (usually 3+) using the orifices and limbs of a single partner
|
||||
id: 47
|
||||
- name: handjob / fingering
|
||||
description: stimulating genitals through physical contact of the hands or fingers
|
||||
id: 48
|
||||
- name: fisting
|
||||
description: penetration (often anally) involving a fist
|
||||
id: 49
|
||||
- name: training (penetration)
|
||||
description: being acclimated to an insertion one wouldn't otherwise be comfortable taking
|
||||
id: 50
|
||||
- name: enemas
|
||||
description: filling intestines with liquid, typically warm water, often to clean them out
|
||||
id: 51
|
||||
|
||||
- name: oral
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: fellatio / cunnilingus
|
||||
description: stimulating genitals through physical contact of the mouth or tongue
|
||||
id: 52
|
||||
- name: rimming
|
||||
description: oral stimulation of one's anus, using the tongue to lick or penetrate
|
||||
id: 53
|
||||
- name: gaping
|
||||
description: one's orifice being stretched far enough that it temporarily stays open
|
||||
id: 54
|
||||
- name: face-fucking
|
||||
description: rhythmic (often rough) penetration of one's mouth
|
||||
id: 55
|
||||
- name: deep throat
|
||||
description: penetration of one's mouth that enters/surpasses the back of the mouth
|
||||
id: 56
|
||||
|
||||
- name: toys (penetration)
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: dildos
|
||||
description: toy shaped like an erect penis, often for usage in penetration
|
||||
id: 57
|
||||
- name: plugs
|
||||
description: toy designed to be inserted into the rectum and keep it shut
|
||||
id: 58
|
||||
- name: anal beads
|
||||
description: small balls; either worn or rhythmically inserted/removed
|
||||
id: 59
|
||||
- name: strap-ons
|
||||
description: a wearable dildo, often used by partners who don't have a penis or don't wish to use it
|
||||
id: 60
|
||||
- name: vibrators
|
||||
description: vibrating toy usually used on the genitals
|
||||
id: 61
|
||||
- name: sounding
|
||||
description: inserting rods (typically made of metal) into the urethra
|
||||
id: 62
|
||||
|
||||
- name: physical control
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: bondage (light)
|
||||
description: for short durations, without major discomfort, and mild amounts of immobilization
|
||||
id: 63
|
||||
- name: bondage (heavy)
|
||||
description: involving extreme duration, level of discomfort, or amount of immobilization
|
||||
id: 64
|
||||
- name: immobilization
|
||||
description: physical inability to move by any means
|
||||
id: 65
|
||||
- name: suspension
|
||||
description: being mid-air above the bed or floor, typically using rope attached to a ceiling hook
|
||||
id: 66
|
||||
- name: collars
|
||||
description: wearing a collar around the neck, often to show ownership, e.g. as part of pet play
|
||||
id: 67
|
||||
- name: gags
|
||||
description: an object placed in the mouth, usually to impede speech
|
||||
id: 68
|
||||
- name: leashing
|
||||
description: being lead around on a leash, often attached to a collar
|
||||
id: 69
|
||||
- name: encasement
|
||||
description: wrapping up the whole body; concealing and often immobilizing
|
||||
id: 70
|
||||
- name: cages
|
||||
description: being locked in a cage, often as a part of pet play or kidnapping
|
||||
id: 71
|
||||
- name: apparatuses
|
||||
description: complex objects with moving parts, often causing a predicament
|
||||
id: 72
|
||||
- name: rope
|
||||
description: being tied up using rope, either by oneself or a partner
|
||||
id: 73
|
||||
- name: hand cuffs
|
||||
description: restraining one's limbs to another or to an object
|
||||
id: 74
|
||||
- name: pinning
|
||||
description: being held down, usually by the wrists, often against a wall or bed
|
||||
id: 75
|
||||
|
||||
- name: no consent / risk
|
||||
spoiler: true
|
||||
kinks:
|
||||
- name: consensual non-consent
|
||||
description: pre-negotiated ignoring of consent as part of a scene
|
||||
id: 76
|
||||
- name: coercion
|
||||
description: manipulations through threats or blackmail
|
||||
id: 77
|
||||
- name: kidnapping
|
||||
description: abduction against one's will
|
||||
id: 78
|
||||
- name: sleep play
|
||||
description: non-consent being achieved through sleep
|
||||
id: 79
|
||||
- name: edgeplay
|
||||
description: activities that are considered to be extreme or dangerous
|
||||
id: 80
|
||||
- name: fear play
|
||||
description: intimidation to create arousal or as a form of coercion
|
||||
id: 81
|
||||
- name: knife play
|
||||
description: sharp objects being used for intimidation or to pierce skin, e.g. to carve a mark
|
||||
id: 82
|
||||
- name: blood
|
||||
description: often as a consequence of a knife piercing skin
|
||||
id: 83
|
||||
- name: breathplay
|
||||
description: restricting airflow
|
||||
id: 84
|
||||
- name: passing out
|
||||
description: losing consciousness, usually as a consequence of breathplay
|
||||
id: 85
|
||||
- name: snuff
|
||||
description: death [or threat thereof]
|
||||
id: 86
|
||||
|
||||
- name: drugs
|
||||
spoiler: true
|
||||
kinks:
|
||||
- name: intoxication
|
||||
description: umbrella term for usage of drugs
|
||||
id: 87
|
||||
- name: sedatives
|
||||
description: typically to disorient, weaken, or incapacitate a partner
|
||||
id: 88
|
||||
- name: stimulants
|
||||
description: typically to enhance stamina and/or pleasure
|
||||
id: 89
|
||||
- name: psychedelics (visual)
|
||||
description: hallucinogenic substances that enhance or distort visual sensations
|
||||
id: 90
|
||||
- name: psychedelics (tactile)
|
||||
description: often to enhance the sensation of touch
|
||||
id: 91
|
||||
- name: dissociatives
|
||||
description: typically to disorient or render unable to resist
|
||||
id: 92
|
||||
- name: alcohol
|
||||
description: typically to disinhibit, render unable to resist, or to boost confidence
|
||||
id: 93
|
||||
- name: amphetamines
|
||||
description: typically to enhance stamina, confidence, and/or immersion
|
||||
id: 94
|
||||
- name: benzodiazepines
|
||||
description: typically to relax, disinhibit, or render unable to resist
|
||||
id: 95
|
||||
- name: ketamine
|
||||
description: typically to disinhibit, disorient, or render unable to resist
|
||||
id: 96
|
||||
- name: weed
|
||||
description: typically to disinhibit or render unable to resist
|
||||
id: 97
|
||||
|
||||
- name: degradation
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: degradation
|
||||
description: having ones sense of self worth lowered
|
||||
id: 98
|
||||
- name: humiliation
|
||||
description: being embarrassed, or being set up to embarrass oneself
|
||||
id: 99
|
||||
- name: extreme humiliation
|
||||
description: intense mental anguish or loss of self-worth; often with lasting psychological damage
|
||||
id: 100
|
||||
- name: public humiliation
|
||||
description: humiliation in a public setting, often explicit and elicit in nature
|
||||
id: 101
|
||||
- name: verbal abuse
|
||||
description: words or names being used demean, degrade or humiliate
|
||||
id: 102
|
||||
- name: begging
|
||||
description: the self-abasing act of pleading, as a form of submission
|
||||
id: 103
|
||||
- name: forced nudity
|
||||
description: forcing a partner to be nude; sometimes in public places
|
||||
id: 104
|
||||
- name: forced orgasm
|
||||
description: e.g. through forced stimulation or using toys; sometimes in public places
|
||||
id: 105
|
||||
- name: cock slapping
|
||||
description: being slapped with a penis, usually as humiliation during fellatio
|
||||
id: 106
|
||||
- name: body writing (degrading)
|
||||
description: having degrading or humiliating messages written on one's body
|
||||
id: 107
|
||||
- name: saliva
|
||||
description: usage of spit, typically as a lubricant or as a form of degradation
|
||||
id: 108
|
||||
|
||||
- name: appreciation
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: praise
|
||||
description: expressions of approval or admiration, typically as encouragement or reward
|
||||
id: 109
|
||||
- name: romance / affection
|
||||
description: expressions of love, strong affection, or passion
|
||||
id: 110
|
||||
- name: cuddling
|
||||
description: showing affection through forms of [gentle] physical closeness
|
||||
id: 111
|
||||
- name: kissing (lips)
|
||||
description: often with tongue
|
||||
id: 112
|
||||
- name: kissing (forehead)
|
||||
description: usually as a form of showing care
|
||||
id: 113
|
||||
- name: kissing (neck)
|
||||
description: usually to tease or as a show of lust
|
||||
id: 114
|
||||
- name: kissing (other body parts)
|
||||
description: often thighs, genitals, belly, chest; usually to show affection
|
||||
id: 115
|
||||
- name: body writing (appreciative)
|
||||
description: often compliments or pet names; sometimes to subvert the expectation of degradation
|
||||
id: 116
|
||||
|
||||
- name: sensation
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: nibbling
|
||||
description: biting gently, often playfully
|
||||
id: 117
|
||||
- name: licking
|
||||
description: sensual stimulation of skin using the tongue
|
||||
id: 118
|
||||
- name: hickeys
|
||||
description: kissing, sucking, or nibbling skin to leave a mark
|
||||
id: 119
|
||||
- name: tickling
|
||||
description: sometimes while bound or as a punishment
|
||||
id: 120
|
||||
- name: scritching
|
||||
description: gentle scratching or rubbing of skin
|
||||
id: 121
|
||||
- name: headpats
|
||||
description: gentle caressing of the head, typically as a show of affection or praise
|
||||
id: 122
|
||||
- name: ice
|
||||
description: typically either for its distinct sensation, to cause pain, or as a numbing agent
|
||||
id: 123
|
||||
- name: electro stimulation
|
||||
description: sex toys that carry an electrical current; sometimes painful
|
||||
id: 124
|
||||
- name: sensory deprivation
|
||||
description: reducing sensory input to enhance sensations or as punishment
|
||||
id: 125
|
||||
- name: blindfolds
|
||||
description: objects obscuring vision, often to enhance other sensations
|
||||
id: 126
|
||||
- name: overstimulation
|
||||
description: high volume or intensity of sensations, sometimes as punishment
|
||||
id: 127
|
||||
|
||||
- name: positions
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: scissoring / frotting
|
||||
description: non-penetrative sex involving direct genital-to-genital contact
|
||||
id: 128
|
||||
- name: docking
|
||||
description: inserting the glans penis into the foreskin of another penis
|
||||
id: 129
|
||||
- name: hotdogging
|
||||
description: placing a penis between butt cheeks and pressing them against it
|
||||
id: 130
|
||||
- name: face-Sitting
|
||||
description: either with buttocks or genitals on the face, often to asphyxiate or force oral sex
|
||||
id: 131
|
||||
|
||||
- name: body parts
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: physical worship
|
||||
description: positive attention (touch, praise) toward physical features (e.g. muscles or genitals)
|
||||
id: 132
|
||||
- name: ears
|
||||
description: suckling, caressing, breathing/whispering into, or pulling them
|
||||
id: 133
|
||||
- name: bellybutton
|
||||
description: often referring to licking or tonguing of the navel
|
||||
id: 134
|
||||
- name: feet
|
||||
description: worshiping them, enjoying their sight or smell, or humping them
|
||||
id: 135
|
||||
- name: breasts
|
||||
description: worshiping them, enjoying their sight or touch, or humping them
|
||||
id: 136
|
||||
- name: nipples
|
||||
description: suckling, caressing, biting, pinching, flicking; sometimes as part of teasing/denial
|
||||
id: 137
|
||||
- name: butt
|
||||
description: worshiping it, enjoying its sight or touch, or humping it
|
||||
id: 138
|
||||
- name: armpits
|
||||
description: often related to their smell or the smell of sweat
|
||||
id: 139
|
||||
|
||||
- name: impact play
|
||||
spoiler: false
|
||||
kinks:
|
||||
- name: slapping
|
||||
description: slaps to the face, of any degree or severity
|
||||
id: 140
|
||||
- name: caning
|
||||
description: being struck with a long, flexible cane
|
||||
id: 141
|
||||
- name: flogging / whipping
|
||||
description: being struck with whips, riding crops, paddles, or flogs
|
||||
id: 142
|
||||
- name: beating
|
||||
description: being struck with fists or other blunt objects
|
||||
id: 143
|
||||
- name: spanking
|
||||
description: striking the butt; often as a form of punishment
|
||||
id: 144
|
||||
- name: genital slapping
|
||||
description: often as a form of punishment
|
||||
id: 145
|
||||
|
||||
- name: pain
|
||||
spoiler: true
|
||||
kinks:
|
||||
- name: light pain
|
||||
description: mild in intensity, duration, or location
|
||||
id: 146
|
||||
- name: heavy pain
|
||||
description: this is probably gonna leave a mark
|
||||
id: 147
|
||||
- name: scratching
|
||||
description: typically using fingernails, sometimes leaving marks; not necessarily painful
|
||||
id: 148
|
||||
- name: biting
|
||||
description: teeth being used to leave a mark or pierce skin
|
||||
id: 149
|
||||
- name: hot wax
|
||||
description: typically dripped onto the skin; often as a form of punishment
|
||||
id: 150
|
||||
- name: branding
|
||||
description: applying intense heat in a specific pattern designed to create a mark of ownership
|
||||
id: 151
|
||||
- name: nipple pinching
|
||||
description: using a pressure device that pinches the nipples, e.g. clothespins
|
||||
id: 152
|
||||
- name: hair pulling
|
||||
description: tugging on the hair, often to direct movement of the head
|
||||
id: 153
|
||||
- name: genital torture
|
||||
description: intense pain to the genitals
|
||||
id: 154
|
||||
- name: ballbusting
|
||||
description: harm to the testicles
|
||||
id: 155
|
||||
- name: sexual exhaustion
|
||||
description: genitals/orifices enduring pain from extensive or prolonged use; often continuing despite such
|
||||
id: 156
|
||||
|
||||
- name: cum
|
||||
spoiler: true
|
||||
kinks:
|
||||
- name: facials
|
||||
description: cumming directly onto the face
|
||||
id: 157
|
||||
- name: swallowing
|
||||
description: orally consuming semen
|
||||
id: 158
|
||||
- name: bukkake
|
||||
description: receiving multiple people's semen onto one's body, often face
|
||||
id: 159
|
||||
- name: creampie
|
||||
description: semen being deposited into or onto an exposed, often gaping, orifice
|
||||
id: 160
|
||||
- name: cum bath
|
||||
description: being treated physically with cum, typically in large amounts
|
||||
id: 161
|
||||
- name: cum enemas
|
||||
description: filling up one's intestines using semen
|
||||
id: 162
|
||||
- name: cum marking
|
||||
description: cumming onto someone as a show of ownership
|
||||
id: 163
|
||||
- name: cum milking
|
||||
description: regularly having one's semen collected through manual or mechanical stimulation
|
||||
id: 164
|
||||
- name: cum on clothes
|
||||
description: ejaculating onto clothing or wearing clothing that has been ejaculated on, often in public or for extended periods of time
|
||||
id: 165
|
||||
- name: premature ejaculation
|
||||
description: reaching orgasm very quickly; typically implies eagerness, hypersensitivity, or prolonged teasing
|
||||
id: 166
|
||||
- name: sloppy seconds
|
||||
description: being penetrated by a different partner in the same orifice without removing the previous partner's semen
|
||||
id: 167
|
||||
- name: snowballing
|
||||
description: moving cum between mouthes, e.g. while kissing
|
||||
id: 168
|
||||
|
||||
- name: urine
|
||||
spoiler: true
|
||||
kinks:
|
||||
- name: watersports
|
||||
description: umbrella term for the urine and urination
|
||||
id: 169
|
||||
- name: bathroom control
|
||||
description: losing control over when, how, or where one may urinate
|
||||
id: 170
|
||||
- name: marking
|
||||
description: being urinated on as a mark of ownership
|
||||
id: 171
|
||||
- name: urine enemas
|
||||
description: often by means of being directly urinated into
|
||||
id: 172
|
||||
- name: swallowing urine
|
||||
description: often by means of being directly urinated into, or from a container that just has been
|
||||
id: 173
|
||||
- name: urinal objectification
|
||||
description: being treated like a [public] urinal, including being urinated into, often carelessly
|
||||
id: 174
|
||||
- name: wetting
|
||||
description: urinating in one's clothes, or being urinated on while clothed
|
||||
id: 175
|
||||
- name: bedwetting
|
||||
description: urinating in one's bed, usually after going to sleep following intense hydration
|
||||
id: 176
|
||||
- name: omorashi
|
||||
description: holding one's pee [or being forced to do so] to the point of desperation
|
||||
id: 177
|
||||
- name: omorashi bullying
|
||||
description: being forced to e.g. listen to water sounds, or to fake pee in an attempt to elicit wetting
|
||||
id: 178
|
||||
- name: bladder stimulation
|
||||
description: having one's bladder pushed into while holding one's pee, often to cause wetting
|
||||
id: 179
|
||||
- name: bathroom cuckoldry
|
||||
description: having to watch someone urinate while desperate from not being allowed to do so
|
||||
id: 180
|
||||
- name: urine on clothes
|
||||
description: wearing clothes that have been peed on, often after wetting oneself; sometimes in public
|
||||
id: 181
|
||||
|
||||
- name: scat
|
||||
spoiler: true
|
||||
kinks:
|
||||
- name: scat
|
||||
description: umbrella term for the feces and defecation
|
||||
id: 182
|
||||
- name: scat torture
|
||||
description: feces as a medium of abuse of someone who is unwilling or uninterested in taking part in such actions
|
||||
id: 183
|
||||
- name: soiling
|
||||
description: oneself or a partner defecating in one's underwear, pants, or onto one's clothes
|
||||
id: 184
|
||||
- name: swallowing feces
|
||||
description: orally consuming feces, often by means of directly being defecated into
|
||||
id: 185
|
||||
|
||||
- name: odor
|
||||
spoiler: true
|
||||
kinks:
|
||||
- name: sweat
|
||||
description: usually in relation to its appearance, taste, scent, or tactility
|
||||
id: 186
|
||||
- name: messy
|
||||
description: making a mess, or being in a messy setting, typically with bodily fluids
|
||||
id: 187
|
||||
- name: musk
|
||||
description: typically implies pheromones derived from the crotch, which may lack hygiene
|
||||
id: 188
|
||||
- name: slob
|
||||
description: being without hygiene or uncaring of cleanliness, usually due to laziness
|
||||
- name: crotch Sniffing
|
||||
id: 189
|
||||
- name: crotch sniffing
|
||||
description: often with emphasis on the musk or pheromone-related properties of the scent
|
||||
id: 190
|
||||
- name: farting
|
||||
description: either for the smell or as a form of humiliation
|
||||
id: 191
|
||||
- name: queefing
|
||||
description: the sounds of air expulsion via intercourse
|
||||
id: 192
|
||||
- name: smoking
|
||||
description: either for the smell or as a show of disregard
|
||||
id: 193
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,5 +0,0 @@
|
|||
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||
#
|
||||
# To ban all spiders from the entire site uncomment the next two lines:
|
||||
# User-agent: *
|
||||
# Disallow: /
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -3,8 +3,10 @@ export PORT=11142
|
|||
export PHX_HOST="kink.bio"
|
||||
|
||||
mix deps.get --only prod
|
||||
MIX_ENV=prod mix compile
|
||||
MIX_ENV=prod mix assets.deploy
|
||||
mix compile
|
||||
mix tailwind.install
|
||||
mix esbuild.install
|
||||
mix assets.deploy
|
||||
|
||||
export SECRET_KEY_BASE="$(mix phx.gen.secret)"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue