wip: messing with some stateless stuff #1

Draft
snek wants to merge 10 commits from stateless into main
54 changed files with 1584 additions and 1192 deletions

View file

@ -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
View file

@ -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

View file

@ -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;
@utility bg-pos-0 {
background-position: "0% 0%";
}
.tooltip::before {
@utility bg-pos-100 {
background-position: "100% 100%";
}
@utility bg-size-200 {
background-size: "200% 200%";
}
@utility bg-size-300 {
background-size: "300% 300%";
}
@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;
background-color: var(--ctp-mocha-crust);
background-color: var(--color-ctp-crust);
}
@media (min-width: 768px) {
.tooltip::before {
&:hover::before,
&:active::before {
display: block;
}
@media (width >= 48rem) {
&::before {
min-width: calc(31.5rem - 1.75rem);
}
}
@media (max-width: 767px) {
.tooltip::before {
min-width: calc(100vw - 1.75rem);
@media (width < 48rem) {
&::before {
overflow-x: hidden;
min-width: calc(100vw - 2.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;
@utility fa-primary-* {
--fa-primary-color: --value(--color-*, [color]);
--fa-primary-opacity: --value(integer, [integer])%;
}
.tooltip:hover::before,
.tooltip:active::before {
display: block;
}
}
/* Alerts and form errors used by phx.new */
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.alert p {
margin-bottom: 0;
}
.alert:empty {
display: none;
}
.invalid-feedback {
color: #a94442;
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;
}
100% {
scale: 1;
opacity: 1;
}
}
@keyframes fade-out-scale-keys {
0% {
scale: 1;
opacity: 1;
}
100% {
scale: 0.95;
opacity: 0;
}
}
@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

View file

@ -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
View 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
View file

@ -0,0 +1,8 @@
{
"dependencies": {
"@catppuccin/tailwindcss": "^1.0.0",
"@tailwindcss/forms": "^0.5.11",
"daisyui": "^5.5.14",
"topbar": "^3.0.0"
}
}

View file

@ -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
View 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/**/*"]
}

View file

@ -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));

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -0,0 +1,3 @@
defmodule KinkBio.Mailer do
use Swoosh.Mailer, otp_app: :kink_bio
end

View file

@ -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

View 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

View 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

View file

@ -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>

View 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

View 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

View file

@ -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

View 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

View 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>

View 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>

View file

@ -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 })
})
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>

View file

@ -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>

View file

@ -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

View file

@ -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],

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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);
}

View file

@ -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
#
# 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).
if Mix.env() in [:dev, :test] do
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

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,3 +0,0 @@
defmodule KinkBioWeb.PageView do
use KinkBioWeb, :view
end

70
mix.exs
View file

@ -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

View file

@ -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"},

View file

@ -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

View file

@ -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.

View file

@ -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)"