create gists

This commit is contained in:
zack 2024-10-26 15:01:33 -04:00
parent 79a17290d5
commit 43a8412f06
No known key found for this signature in database
GPG key ID: 5F873416BCF59F35
90 changed files with 1777 additions and 2107 deletions

View file

@ -21,6 +21,55 @@ import "phoenix_html";
import { Socket } from "phoenix"; import { Socket } from "phoenix";
import { LiveSocket } from "phoenix_live_view"; import { LiveSocket } from "phoenix_live_view";
import topbar from "../vendor/topbar"; import topbar from "../vendor/topbar";
import CodeBlockHook from "./hooks/code_block_hook";
import {
DropdownAnimation,
SearchableDropdown,
} from "./hooks/searchable_dropdown";
let Hooks = {
CodeBlockHook,
SearchableDropdown,
DropdownAnimation,
};
Hooks.ClickOutside = {
mounted() {
this.handleClick = (e) => {
if (!this.el.contains(e.target)) {
this.pushEventTo(this.el, "close_dropdown", {});
}
};
document.addEventListener("click", this.handleClick);
},
destroyed() {
document.removeEventListener("click", this.handleClick);
},
};
Hooks.ClientSearch = {
mounted() {
this.options = JSON.parse(this.el.dataset.options);
this.searchInput = this.el.querySelector("input");
this.optionsContainer = this.el.querySelector(
"#dropdown-options-container",
);
this.searchInput.addEventListener("input", (e) => {
const query = e.target.value.toLowerCase();
const options = this.el.querySelectorAll(".dropdown-option");
options.forEach((option) => {
const value = option.dataset.value.toLowerCase();
if (value.includes(query)) {
option.style.display = "";
} else {
option.style.display = "none";
}
});
});
},
};
window.addEventListener("phx:copy", (event) => { window.addEventListener("phx:copy", (event) => {
let button = event.detail.dispatcher; let button = event.detail.dispatcher;
@ -40,6 +89,7 @@ let csrfToken = document
let liveSocket = new LiveSocket("/live", Socket, { let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500, longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken }, params: { _csrf_token: csrfToken },
hooks: Hooks,
}); });
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits

View file

@ -0,0 +1,59 @@
// assets/js/hooks/code_block_hook.js
import { codeToHtml } from "shiki";
const CodeBlockHook = {
mounted() {
const code = this.el.dataset.code;
const language = this.el.dataset.language;
const highlightedLines = JSON.parse(
this.el.dataset.highlightedLines || "[]",
);
console.log(code);
console.log("language", language);
const lines = code.split("\n");
// Convert line numbers to decorations
const decorations = highlightedLines
.map((line) => {
// Convert to 0-based index and ensure valid line number
const lineIndex = line - 1;
if (lineIndex < 0 || lineIndex >= lines.length) return null;
// Get the actual line length
const lineLength = lines[lineIndex].length;
return {
// Line numbers are 0-indexed
start: { line: lineIndex, character: 0 },
end: { line: lineIndex, character: lineLength },
properties: {
// Apply both background color and a class for flexibility
class: "highlight",
style: "background-color: rgba(200,200,255,0.1);",
},
};
})
.filter(Boolean); // Remove any null entries from invalid line numbers
codeToHtml(code, {
lang: language,
theme: "catppuccin-mocha",
decorations,
}).then((html) => {
console.log(html);
// Replace the code content while preserving the pre/code structure
const tempDiv = document.createElement("div");
tempDiv.innerHTML = html;
const codeContent = tempDiv.querySelector("code");
if (codeContent) {
this.el.querySelector("code").innerHTML = codeContent.innerHTML;
}
});
},
};
export default CodeBlockHook;

View file

@ -0,0 +1,32 @@
SearchableDropdown = {
mounted() {
this.el.addEventListener("input", (e) => {
const query = e.target.value.toLowerCase();
const dropdownId = this.el.dataset.dropdownId;
const optionsContainer = document.querySelector(`#${dropdownId}-options`);
const options = optionsContainer.querySelectorAll("li button");
options.forEach((option) => {
const text = option.textContent.toLowerCase();
option.parentElement.style.display = text.includes(query)
? "block"
: "none";
});
});
},
};
const DropdownAnimation = {
mounted() {
this.el.addEventListener("transitionend", (e) => {
if (
e.propertyName === "opacity" &&
this.el.classList.contains("fade-out")
) {
this.el.classList.add("hidden");
}
});
},
};
export { SearchableDropdown, DropdownAnimation };

File diff suppressed because it is too large Load diff

View file

@ -10,11 +10,13 @@
"@esbuild/linux-x64": "^0.24.0", "@esbuild/linux-x64": "^0.24.0",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"i": "^0.3.7",
"phoenix": "^1.7.14", "phoenix": "^1.7.14",
"phoenix_html": "^3.3.4", "phoenix_html": "^3.3.4",
"phoenix_live_view": "^1.0.0-rc.7", "phoenix_live_view": "^1.0.0-rc.7",
"shiki": "^1.22.0", "shiki": "^1.22.1",
"tailwindcss": "^3.4.14" "tailwindcss": "^3.4.14",
"tailwindcss-animate": "^1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@types/phoenix": "^1.6.5", "@types/phoenix": "^1.6.5",
@ -536,44 +538,44 @@
} }
}, },
"node_modules/@shikijs/core": { "node_modules/@shikijs/core": {
"version": "1.22.0", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.0.tgz", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.1.tgz",
"integrity": "sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==", "integrity": "sha512-bqAhT/Ri5ixV4oYsvJNH8UJjpjbINWlWyXY6tBTsP4OmD6XnFv43nRJ+lTdxd2rmG5pgam/x+zGR6kLRXrpEKA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/engine-javascript": "1.22.0", "@shikijs/engine-javascript": "1.22.1",
"@shikijs/engine-oniguruma": "1.22.0", "@shikijs/engine-oniguruma": "1.22.1",
"@shikijs/types": "1.22.0", "@shikijs/types": "1.22.1",
"@shikijs/vscode-textmate": "^9.3.0", "@shikijs/vscode-textmate": "^9.3.0",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.3" "hast-util-to-html": "^9.0.3"
} }
}, },
"node_modules/@shikijs/engine-javascript": { "node_modules/@shikijs/engine-javascript": {
"version": "1.22.0", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz", "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.1.tgz",
"integrity": "sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==", "integrity": "sha512-540pyoy0LWe4jj2BVbgELwOFu1uFvRI7lg4hdsExrSXA9x7gqfzZ/Nnh4RfX86aDAgJ647gx4TCmRwACbnQSvw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/types": "1.22.0", "@shikijs/types": "1.22.1",
"@shikijs/vscode-textmate": "^9.3.0", "@shikijs/vscode-textmate": "^9.3.0",
"oniguruma-to-js": "0.4.3" "oniguruma-to-js": "0.4.3"
} }
}, },
"node_modules/@shikijs/engine-oniguruma": { "node_modules/@shikijs/engine-oniguruma": {
"version": "1.22.0", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.1.tgz",
"integrity": "sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==", "integrity": "sha512-L+1Vmd+a2kk8HtogUFymQS6BjUfJnzcWoUp1BUgxoDiklbKSMvrsMuLZGevTOP1m0rEjgnC5MsDmsr8lX1lC+Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/types": "1.22.0", "@shikijs/types": "1.22.1",
"@shikijs/vscode-textmate": "^9.3.0" "@shikijs/vscode-textmate": "^9.3.0"
} }
}, },
"node_modules/@shikijs/types": { "node_modules/@shikijs/types": {
"version": "1.22.0", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.0.tgz", "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.1.tgz",
"integrity": "sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==", "integrity": "sha512-+45f8mu/Hxqs6Kyhfm98Nld5n7Q7lwhjU8UtdQwrOPs7BnM4VAb929O3IQ2ce+4D7SlNFlZGd8CnKRSnwbQreQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/vscode-textmate": "^9.3.0", "@shikijs/vscode-textmate": "^9.3.0",
@ -1134,6 +1136,14 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/i": {
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz",
"integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/is-binary-path": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -1858,15 +1868,15 @@
} }
}, },
"node_modules/shiki": { "node_modules/shiki": {
"version": "1.22.0", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.0.tgz", "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.1.tgz",
"integrity": "sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==", "integrity": "sha512-PbJ6XxrWLMwB2rm3qdjIHNm3zq4SfFnOx0B3rEoi4AN8AUngsdyZ1tRe5slMPtn6jQkbUURLNZPpLR7Do3k78g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/core": "1.22.0", "@shikijs/core": "1.22.1",
"@shikijs/engine-javascript": "1.22.0", "@shikijs/engine-javascript": "1.22.1",
"@shikijs/engine-oniguruma": "1.22.0", "@shikijs/engine-oniguruma": "1.22.1",
"@shikijs/types": "1.22.0", "@shikijs/types": "1.22.1",
"@shikijs/vscode-textmate": "^9.3.0", "@shikijs/vscode-textmate": "^9.3.0",
"@types/hast": "^3.0.4" "@types/hast": "^3.0.4"
} }
@ -2083,6 +2093,15 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
"integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
"license": "MIT",
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/thenify": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",

View file

@ -11,11 +11,13 @@
"@esbuild/linux-x64": "^0.24.0", "@esbuild/linux-x64": "^0.24.0",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"i": "^0.3.7",
"phoenix": "^1.7.14", "phoenix": "^1.7.14",
"phoenix_html": "^3.3.4", "phoenix_html": "^3.3.4",
"phoenix_live_view": "^1.0.0-rc.7", "phoenix_live_view": "^1.0.0-rc.7",
"shiki": "^1.22.0", "shiki": "^1.22.1",
"tailwindcss": "^3.4.14" "tailwindcss": "^3.4.14",
"tailwindcss-animate": "^1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@types/phoenix": "^1.6.5", "@types/phoenix": "^1.6.5",

View file

@ -12,7 +12,45 @@ module.exports = {
"../lib/zoeyscomputer_web/**/*.*ex", "../lib/zoeyscomputer_web/**/*.*ex",
], ],
theme: { theme: {
fontSize: {
sm: "1rem",
base: "1.2rem",
xl: "1.45rem",
"2xl": "1.563rem",
"3xl": "1.953rem",
"4xl": "2.441rem",
"5xl": "3.052rem",
},
fontFamily: {
sans: ["Iosevka Web", "monospace"],
},
extend: { extend: {
keyframes: {
"fade-in": {
"0%": { opacity: 0 },
"100%": { opacity: 1 },
},
"fade-out": {
"0%": { opacity: 1 },
"100%": { opacity: 0 },
},
"zoom-in": {
"0%": { transform: "scale(0.95)" },
"100%": { transform: "scale(1)" },
},
"zoom-out": {
"0%": { transform: "scale(1)" },
"100%": { transform: "scale(0.95)" },
},
},
animation: {
"fade-in-0": "fade-in 0.2s ease-in-out",
"fade-in": "fade-in 0.3s ease-in-out",
"fade-out-0": "fade-out 0.2s ease-in-out",
"fade-out": "fade-out 0.3s ease-in-out",
"zoom-in-95": "zoom-in 0.2s ease-in-out",
"zoom-out-95": "zoom-out 0.2s ease-in-out",
},
colors: { colors: {
brand: "#FD4F00", brand: "#FD4F00",
}, },
@ -25,6 +63,7 @@ module.exports = {
// //
// <div class="phx-click-loading:animate-ping"> // <div class="phx-click-loading:animate-ping">
// //
require("tailwindcss-animate"),
require("@catppuccin/tailwindcss")({ require("@catppuccin/tailwindcss")({
prefix: "ctp", prefix: "ctp",
defaultFlavor: "mocha", defaultFlavor: "mocha",

104
lib/zoeyscomputer/gists.ex Normal file
View file

@ -0,0 +1,104 @@
defmodule Zoeyscomputer.Gists do
@moduledoc """
The Gists context.
"""
import Ecto.Query, warn: false
alias Zoeyscomputer.Repo
alias Zoeyscomputer.Gists.Gist
@doc """
Returns the list of gists.
## Examples
iex> list_gists()
[%Gist{}, ...]
"""
def list_gists do
Repo.all(Gist)
end
@doc """
Gets a single gist.
Raises `Ecto.NoResultsError` if the Gist does not exist.
## Examples
iex> get_gist!(123)
%Gist{}
iex> get_gist!(456)
** (Ecto.NoResultsError)
"""
def get_gist!(id), do: Repo.get!(Gist, id)
@doc """
Creates a gist.
## Examples
iex> create_gist(%{field: value})
{:ok, %Gist{}}
iex> create_gist(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_gist(attrs \\ %{}) do
%Gist{}
|> Gist.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a gist.
## Examples
iex> update_gist(gist, %{field: new_value})
{:ok, %Gist{}}
iex> update_gist(gist, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_gist(%Gist{} = gist, attrs) do
gist
|> Gist.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a gist.
## Examples
iex> delete_gist(gist)
{:ok, %Gist{}}
iex> delete_gist(gist)
{:error, %Ecto.Changeset{}}
"""
def delete_gist(%Gist{} = gist) do
Repo.delete(gist)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking gist changes.
## Examples
iex> change_gist(gist)
%Ecto.Changeset{data: %Gist{}}
"""
def change_gist(%Gist{} = gist, attrs \\ %{}) do
Gist.changeset(gist, attrs)
end
end

View file

@ -0,0 +1,18 @@
defmodule Zoeyscomputer.Gists.Gist do
use Ecto.Schema
import Ecto.Changeset
schema "gists" do
field :code, :string
field :lang, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(gist, attrs) do
gist
|> cast(attrs, [:code, :lang])
|> validate_required([:code, :lang])
end
end

View file

@ -0,0 +1,93 @@
defmodule ZoeyscomputerWeb.CodeBlock do
use Phoenix.Component
alias Phoenix.LiveView.JS
@moduledoc """
A code block component with syntax highlighting using Shiki.
## Features:
- Syntax highlighting with Shiki
- Optional line numbers
- Optional highlighted lines
- Optional title
- Copy button
- Catppuccin theme styling
"""
@doc """
Renders a code block with syntax highlighting.
## Examples
<.code_block
code="def hello, do: :world"
language="elixir"
title="Example Code"
line_numbers={true}
highlighted_lines={[1, 3]}
/>
## Options
* `:code` - Required. The code string to highlight
* `:language` - Required. Programming language for syntax highlighting
* `:title` - Optional. Title displayed above the code block
* `:line_numbers` - Optional. Show line numbers (default: false)
* `:highlighted_lines` - Optional. List of line numbers to highlight
"""
attr :code, :string, required: true
attr :language, :string, required: true
attr :title, :string, default: nil
attr :line_numbers, :boolean, default: false
attr :highlighted_lines, :list, default: []
def code_block(assigns) do
# Calculate the number of lines for line numbers
assigns = assign(assigns, :num_lines, String.split(assigns.code, "\n") |> length())
~H"""
<div class="relative ctp-bg-base rounded-lg overflow-hidden">
<%= if @title do %>
<div class="ctp-bg-mantle px-4 py-2 border-b ctp-border-surface0">
<h3 class="ctp-text-text text-sm font-medium"><%= @title %></h3>
</div>
<% end %>
<div class="relative">
<%= if @line_numbers do %>
<div class="absolute left-0 top-0 bottom-0 ctp-bg-crust w-12 flex flex-col items-end pr-2 py-4 ctp-text-surface2 select-none">
<%= for line_num <- 1..@num_lines do %>
<span class={[
"text-sm leading-6",
line_num in @highlighted_lines && "ctp-text-mauve font-medium"
]}>
<%= line_num %>
</span>
<% end %>
</div>
<% end %>
<div class={["overflow-x-auto", @line_numbers && "pl-12"]}>
<pre
class="p-4"
id={"code-block-#{System.unique_integer()}"}
phx-hook="CodeBlockHook"
data-code={@code}
data-language={@language}
data-highlighted-lines={Jason.encode!(@highlighted_lines)}
><code class="text-sm"><%= @code %></code></pre>
</div>
</div>
<div class="ctp-bg-mantle px-4 py-2 border-t ctp-border-surface0 flex justify-end">
<button
type="button"
class="ctp-text-subtext0 hover:ctp-text-text text-sm"
phx-click={JS.dispatch("clipcopy", detail: %{text: @code})}
>
Copy code
</button>
</div>
</div>
"""
end
end

View file

@ -1,29 +1,3 @@
<%!-- <header class="px-4 sm:px-6 lg:px-8"> --%>
<%!-- <div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm"> --%>
<%!-- <div class="flex items-center gap-4"> --%>
<%!-- <a href="/"> --%>
<%!-- <img src={~p"/images/logo.svg"} width="36" /> --%>
<%!-- </a> --%>
<%!-- <p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6"> --%>
<%!-- v<%= Application.spec(:phoenix, :vsn) %> --%>
<%!-- </p> --%>
<%!-- </div> --%>
<%!-- <div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900"> --%>
<%!-- <a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700"> --%>
<%!-- @elixirphoenix --%>
<%!-- </a> --%>
<%!-- <a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700"> --%>
<%!-- GitHub --%>
<%!-- </a> --%>
<%!-- <a --%>
<%!-- href="https://hexdocs.pm/phoenix/overview.html" --%>
<%!-- class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80" --%>
<%!-- > --%>
<%!-- Get Started <span aria-hidden="true">&rarr;</span> --%>
<%!-- </a> --%>
<%!-- </div> --%>
<%!-- </div> --%>
<%!-- </header> --%>
<main class="bg-ctp-base px-4 py-20 sm:px-6 lg:px-8"> <main class="bg-ctp-base px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} /> <.flash_group flash={@flash} />

View file

@ -8,20 +8,37 @@
<%= assigns[:page_title] || "" %> <%= assigns[:page_title] || "" %>
</.live_title> </.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} /> <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<link phx-track-statuc rel="stylesheet" href={~p"/fonts/Iosevka.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}> <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script> </script>
</head> </head>
<body class="ctp-mocha bg-ctp-base text-ctp-overlay0"> <body class="ctp-mocha bg-ctp-base text-base text-ctp-overlay0">
<%= if @current_user do %> <%= if @current_user do %>
<div class="flex w-full"> <div class="flex w-full">
<div class="p-4 grow"> <div class="p-4">
<.link <.link
class="text-ctp-mauve font-bold hover:border-b-2 border-ctp-mauve" class="text-ctp-mauve text-sm font-bold hover:border-b-2 border-ctp-mauve"
navigate={~p"/"} navigate={~p"/"}
> >
zoey zoey
</.link> </.link>
</div> </div>
<div class="p-4">
<.link
class="text-ctp-pink text-sm hover:border-b-2 border-ctp-pink"
navigate={~p"/images"}
>
image
</.link>
</div>
<div class="p-4 grow">
<.link
class="text-ctp-pink text-sm hover:border-b-2 border-ctp-pink"
navigate={~p"/gists"}
>
gists
</.link>
</div>
<ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end"> <ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<li class="text-[0.8125rem] leading-6"> <li class="text-[0.8125rem] leading-6">
<%= @current_user.email %> <%= @current_user.email %>

View file

@ -0,0 +1,81 @@
defmodule ZoeyscomputerWeb.SearchableDropdown do
use Phoenix.Component
alias Phoenix.LiveView.JS
attr :id, :string, required: true
attr :options, :list, required: true
attr :selected, :string, default: nil
attr :class, :string, default: nil
attr :name, :string, required: true
attr :form, :any, required: true
def searchable_dropdown(assigns) do
~H"""
<div class={["relative w-full", @class]} id={"#{@id}-container"}>
<div class="relative" phx-click-away={JS.hide(to: "##{@id}-dropdown", transition: "fade-out")}>
<input type="hidden" name={@name} value={@selected} id={"#{@id}-input"} />
<button
type="button"
class="flex w-full items-center justify-between rounded-md border border-ctp-surface0 bg-ctp-base px-3 py-2 text-sm text-ctp-text shadow-sm hover:bg-ctp-surface0 focus:outline-none focus:ring-2 focus:ring-ctp-lavender transition-colors duration-200"
phx-click={
JS.toggle(to: "##{@id}-dropdown", in: "fade-in", out: "fade-out")
|> JS.focus(to: "##{@id}-search")
}
aria-haspopup="listbox"
aria-expanded="false"
>
<span class="block truncate">
<%= @selected || "Select an option..." %>
</span>
<svg
class="h-5 w-5 text-ctp-overlay0 transform transition-transform duration-200"
class={"#{if @selected, do: "rotate-180", else: ""}"}
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3z"
clip-rule="evenodd"
/>
</svg>
</button>
<div
id={"#{@id}-dropdown"}
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border border-ctp-surface0 bg-ctp-base shadow-lg hidden transition-all duration-200 ease-in-out"
phx-hook="DropdownAnimation"
>
<div class="p-2">
<input
type="text"
id={"#{@id}-search"}
placeholder="Search..."
class="w-full rounded-md border border-ctp-surface0 bg-ctp-mantle px-3 py-2 text-sm text-ctp-text placeholder-ctp-overlay0 focus:outline-none focus:ring-2 focus:ring-ctp-lavender transition-colors duration-200"
phx-hook="SearchableDropdown"
data-dropdown-id={@id}
data-form-id={@form.id}
/>
</div>
<ul class="max-h-48 overflow-y-auto py-1" role="listbox" id={"#{@id}-options"}>
<li :for={option <- @options} class="transition-colors duration-150 ease-in-out">
<button
type="button"
class="w-full px-3 py-2 text-left text-sm hover:bg-ctp-surface0 focus:bg-ctp-surface0 focus:outline-none cursor-pointer transition-colors duration-150 ease-in-out"
phx-click={
JS.push("select_language", value: %{language: option}, target: "##{@form.id}")
|> JS.hide(to: "##{@id}-dropdown", transition: "fade-out")
}
phx-target={"##{@form.id}"}
role="option"
data-option={option}
>
<%= option %>
</button>
</li>
</ul>
</div>
</div>
</div>
"""
end
end

View file

@ -0,0 +1,43 @@
defmodule ZoeyscomputerWeb.GistController do
use ZoeyscomputerWeb, :controller
alias Zoeyscomputer.Gists
alias Zoeyscomputer.Gists.Gist
action_fallback ZoeyscomputerWeb.FallbackController
def index(conn, _params) do
gists = Gists.list_gists()
render(conn, :index, gists: gists)
end
def create(conn, %{"gist" => gist_params}) do
with {:ok, %Gist{} = gist} <- Gists.create_gist(gist_params) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/api/gists/#{gist}")
|> render(:show, gist: gist)
end
end
def show(conn, %{"id" => id}) do
gist = Gists.get_gist!(id)
render(conn, :show, gist: gist)
end
def update(conn, %{"id" => id, "gist" => gist_params}) do
gist = Gists.get_gist!(id)
with {:ok, %Gist{} = gist} <- Gists.update_gist(gist, gist_params) do
render(conn, :show, gist: gist)
end
end
def delete(conn, %{"id" => id}) do
gist = Gists.get_gist!(id)
with {:ok, %Gist{}} <- Gists.delete_gist(gist) do
send_resp(conn, :no_content, "")
end
end
end

View file

@ -0,0 +1,25 @@
defmodule ZoeyscomputerWeb.GistJSON do
alias Zoeyscomputer.Gists.Gist
@doc """
Renders a list of gists.
"""
def index(%{gists: gists}) do
%{data: for(gist <- gists, do: data(gist))}
end
@doc """
Renders a single gist.
"""
def show(%{gist: gist}) do
%{data: data(gist)}
end
defp data(%Gist{} = gist) do
%{
id: gist.id,
code: gist.code,
lang: gist.lang
}
end
end

View file

@ -1,9 +0,0 @@
<div class="container p-4 flex justify-center align-middle">
<div class="border border-ctp-overlay0 rounded-md p-4 flex-col flex items-center">
<h1 class="font-bold text-ctp-mauve">zoey</h1>
<p class="text-ctp-text"><i>Software Engineer 🏳️‍⚧️</i></p>
<p class="max-w-96 text-center mt-4 text-ctp-overlay2">
Currently cooking this up, stay tuned... in the meantime, you can monitor my server's resources.
</p>
</div>
</div>

View file

@ -0,0 +1,83 @@
defmodule ZoeyscomputerWeb.GistLive.FormComponent do
use ZoeyscomputerWeb, :live_component
alias Zoeyscomputer.Gists
@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage gist records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="gist-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:code]} type="text" label="Code" />
<.input field={@form[:lang]} type="text" label="Lang" />
<:actions>
<.button phx-disable-with="Saving...">Save Gist</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{gist: gist} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_new(:form, fn ->
to_form(Gists.change_gist(gist))
end)}
end
@impl true
def handle_event("validate", %{"gist" => gist_params}, socket) do
changeset = Gists.change_gist(socket.assigns.gist, gist_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def handle_event("save", %{"gist" => gist_params}, socket) do
save_gist(socket, socket.assigns.action, gist_params)
end
defp save_gist(socket, :edit, gist_params) do
case Gists.update_gist(socket.assigns.gist, gist_params) do
{:ok, gist} ->
notify_parent({:saved, gist})
{:noreply,
socket
|> put_flash(:info, "Gist updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp save_gist(socket, :new, gist_params) do
case Gists.create_gist(gist_params) do
{:ok, gist} ->
notify_parent({:saved, gist})
{:noreply,
socket
|> put_flash(:info, "Gist created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

View file

@ -0,0 +1,47 @@
defmodule ZoeyscomputerWeb.GistLive.Index do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Gists
alias Zoeyscomputer.Gists.Gist
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :gists, Gists.list_gists())}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Gist")
|> assign(:gist, Gists.get_gist!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Gist")
|> assign(:gist, %Gist{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Gists")
|> assign(:gist, nil)
end
@impl true
def handle_info({ZoeyscomputerWeb.GistLive.FormComponent, {:saved, gist}}, socket) do
{:noreply, stream_insert(socket, :gists, gist)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
gist = Gists.get_gist!(id)
{:ok, _} = Gists.delete_gist(gist)
{:noreply, stream_delete(socket, :gists, gist)}
end
end

View file

@ -0,0 +1,42 @@
<.header>
Listing Gists
<:actions>
<.link patch={~p"/gists/new"}>
<.button>New Gist</.button>
</.link>
</:actions>
</.header>
<.table
id="gists"
rows={@streams.gists}
row_click={fn {_id, gist} -> JS.navigate(~p"/gists/#{gist}") end}
>
<:col :let={{_id, gist}} label="Code"><%= gist.code %></:col>
<:col :let={{_id, gist}} label="Lang"><%= gist.lang %></:col>
<:action :let={{_id, gist}}>
<div class="sr-only">
<.link navigate={~p"/gists/#{gist}"}>Show</.link>
</div>
<.link patch={~p"/gists/#{gist}/edit"}>Edit</.link>
</:action>
<:action :let={{id, gist}}>
<.link
phx-click={JS.push("delete", value: %{id: gist.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
<.modal :if={@live_action in [:new, :edit]} id="gist-modal" show on_cancel={JS.patch(~p"/gists")}>
<.live_component
module={ZoeyscomputerWeb.GistLive.FormComponent}
id={@gist.id || :new}
title={@page_title}
action={@live_action}
gist={@gist}
patch={~p"/gists"}
/>
</.modal>

View file

@ -0,0 +1,21 @@
defmodule ZoeyscomputerWeb.GistLive.Show do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Gists
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:gist, Gists.get_gist!(id))}
end
defp page_title(:show), do: "Show Gist"
defp page_title(:edit), do: "Edit Gist"
end

View file

@ -0,0 +1,27 @@
<.header>
Gist <%= @gist.id %>
<:subtitle>This is a gist record from your database.</:subtitle>
<:actions>
<.link patch={~p"/gists/#{@gist}/show/edit"} phx-click={JS.push_focus()}>
<.button>Edit gist</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Code"><%= @gist.code %></:item>
<:item title="Lang"><%= @gist.lang %></:item>
</.list>
<.back navigate={~p"/gists"}>Back to gists</.back>
<.modal :if={@live_action == :edit} id="gist-modal" show on_cancel={JS.patch(~p"/gists/#{@gist}")}>
<.live_component
module={ZoeyscomputerWeb.GistLive.FormComponent}
id={@gist.id}
title={@page_title}
action={@live_action}
gist={@gist}
patch={~p"/gists/#{@gist}"}
/>
</.modal>

View file

@ -1,5 +1,4 @@
defmodule ZoeyscomputerWeb.HomeLive do defmodule ZoeyscomputerWeb.HomeLive do
alias Hex.API.Key
use ZoeyscomputerWeb, :live_view use ZoeyscomputerWeb, :live_view
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -40,8 +39,8 @@ defmodule ZoeyscomputerWeb.HomeLive do
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div class="container p-4 flex justify-center align-middle"> <div class="container p-8 flex justify-center align-middle">
<div class="border border-ctp-overlay0 rounded-md p-4 flex-col flex items-center"> <div class="border border-ctp-overlay0 rounded-md p-8 flex-col flex items-center">
<h1 class="font-bold text-ctp-mauve">zoey</h1> <h1 class="font-bold text-ctp-mauve">zoey</h1>
<p class="text-ctp-text"><i>Software Engineer 🏳</i></p> <p class="text-ctp-text"><i>Software Engineer 🏳</i></p>
<p class="max-w-96 text-center mt-4 text-ctp-overlay2"> <p class="max-w-96 text-center mt-4 text-ctp-overlay2">

View file

@ -1,82 +0,0 @@
defmodule ZoeyscomputerWeb.LinkLive.FormComponent do
use ZoeyscomputerWeb, :live_component
alias Zoeyscomputer.Links
@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage link records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="link-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:url]} type="text" label="Url" />
<:actions>
<.button phx-disable-with="Saving...">Save Link</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{link: link} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_new(:form, fn ->
to_form(Links.change_link(link))
end)}
end
@impl true
def handle_event("validate", %{"link" => link_params}, socket) do
changeset = Links.change_link(socket.assigns.link, link_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def handle_event("save", %{"link" => link_params}, socket) do
save_link(socket, socket.assigns.action, link_params)
end
defp save_link(socket, :edit, link_params) do
case Links.update_link(socket.assigns.link, link_params) do
{:ok, link} ->
notify_parent({:saved, link})
{:noreply,
socket
|> put_flash(:info, "Link updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp save_link(socket, :new, link_params) do
case Links.create_link(link_params) do
{:ok, link} ->
notify_parent({:saved, link})
{:noreply,
socket
|> put_flash(:info, "Link created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

View file

@ -1,39 +0,0 @@
defmodule ZoeyscomputerWeb.LinkLive.Index do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Links
def mount(_params, _session, socket) do
user_id = socket.assigns.current_user.id
changeset = Links.Link.changeset(%Links.Link{})
socket =
socket
|> assign(:links, Links.list_links(user_id))
|> assign(:form, to_form(changeset))
{:ok, socket}
end
def handle_event("submit", %{"link" => link_params}, socket) do
params =
link_params
|> Map.put("user_id", socket.assigns.current_user.id)
case Links.create_link(params) do
{:ok, link} ->
socket =
socket
|> assign(:links, [link | socket.assigns.links])
{:noreply, socket}
{:error, changeset} ->
socket
|> assign(:form, to_form(changeset))
{:noreply, socket}
end
end
end

View file

@ -1,30 +0,0 @@
<div class="flex gap-2">
<h1 class="text-2xl grow font-bold">Links</h1>
<.link
navigate={~p"/links/new"}
class="bg-black border border-black hover:bg-gray-700 text-white font-bold py-2 px-3 rounded-md"
>
Add Link
</.link>
</div>
<div class="divide-y">
<div :for={link <- @links}>
<div>
<div class="font-bold"><%= link.url %></div>
<div class="text-sm"><%= link.inserted_at %></div>
</div>
</div>
</div>
<.form for={@form} phx-submit="submit">
<div class="flex gap-2 items-end">
<div class="grow">
<.input field={@form[:url]} type="text" label="url" />
</div>
<button class="bg-black border border-black hover:bg-gray-700 text-white font-bold py-2 px-3 rounded-md">
Create
</button>
</div>
</.form>

View file

@ -1,37 +0,0 @@
defmodule ZoeyscomputerWeb.LinkLive.New do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Links
def mount(_params, _session, socket) do
changeset = Links.Link.changeset(%Links.Link{})
socket =
socket
|> assign(:form, to_form(changeset))
{:ok, socket}
end
def handle_event("submit", %{"link" => link_params}, socket) do
params =
link_params
|> Map.put("user_id", socket.assigns.current_user.id)
case Links.create_link(params) do
{:ok, _link} ->
socket =
socket
|> put_flash(:info, "Link created successfully")
|> push_navigate(to: ~p"/links")
{:noreply, socket}
{:error, changeset} ->
socket
|> assign(:form, to_form(changeset))
{:noreply, socket}
end
end
end

View file

@ -1,12 +0,0 @@
<h1 class="text-2xl grow font-bold mb-6">Create a new link</h1>
<.form for={@form} phx-submit="submit">
<div class="flex gap-2 items-end">
<div class="grow">
<.input field={@form[:url]} type="text" label="url" />
</div>
<button class="bg-black border border-black hover:bg-gray-700 text-white font-bold py-2 px-3 rounded-md">
Create
</button>
</div>
</.form>

View file

@ -1,6 +1,5 @@
defmodule ZoeyscomputerWeb.DiscordHandler do defmodule ZoeyscomputerWeb.DiscordHandler do
require Logger require Logger
alias ElixirSense.Log
alias ExAws.S3 alias ExAws.S3
import Plug.Conn import Plug.Conn
import Mogrify import Mogrify
@ -15,7 +14,7 @@ defmodule ZoeyscomputerWeb.DiscordHandler do
def call(%{path_info: ["images", _id | _]} = conn, _opts) do def call(%{path_info: ["images", _id | _]} = conn, _opts) do
user_agent = List.first(get_req_header(conn, "user-agent")) user_agent = List.first(get_req_header(conn, "user-agent"))
request_id = Logger.metadata()[:request_id] Logger.metadata()[:request_id]
Logger.info("Processing image request:\n user_agent: #{user_agent}") Logger.info("Processing image request:\n user_agent: #{user_agent}")

View file

@ -1,6 +1,5 @@
defmodule ZoeyscomputerWeb.Router do defmodule ZoeyscomputerWeb.Router do
require Logger require Logger
alias ExAws.S3
use ZoeyscomputerWeb, :router use ZoeyscomputerWeb, :router
import ZoeyscomputerWeb.UserAuth import ZoeyscomputerWeb.UserAuth
@ -29,9 +28,11 @@ defmodule ZoeyscomputerWeb.Router do
pipe_through :api pipe_through :api
resources "/images", ImageController, except: [:create, :edit] resources "/images", ImageController, except: [:create, :edit]
resources "/gists", GistController, except: [:new, :edit]
pipe_through [:api_authentication] pipe_through [:api_authentication]
post "/images/create", ImageController, :create post "/images/create", ImageController, :create
post "/gists/create", GistController, :create
end end
# Enable LiveDashboard and Swoosh mailbox preview in development # Enable LiveDashboard and Swoosh mailbox preview in development
@ -77,15 +78,17 @@ defmodule ZoeyscomputerWeb.Router do
on_mount: [{ZoeyscomputerWeb.UserAuth, :ensure_authenticated}] do on_mount: [{ZoeyscomputerWeb.UserAuth, :ensure_authenticated}] do
live "/users/settings", UserSettingsLive, :edit live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
live "/links", LinkLive.Index
live "/links/new", LinkLive.New
live "/images/new", ImageLive.Index, :new live "/images/new", ImageLive.Index, :new
live "/gists/new", GistLive.Index, :new
live "/images/:id/edit", ImageLive.Index, :edit live "/images/:id/edit", ImageLive.Index, :edit
live "/gists/:id/edit", GistLive.Index, :edit
live "/images", ImageLive.Index, :index live "/images", ImageLive.Index, :index
live "/gists", GistLive.Index
live "/images/:id/show/edit", ImageLive.Show, :edit live "/images/:id/show/edit", ImageLive.Show, :edit
live "/gists/:id/show/edit", GistLive.Show, :edit
live "/api-keys", ApiKeyLive.Index, :index live "/api-keys", ApiKeyLive.Index, :index
live "/api-keys/new", ApiKeyLive.Index, :new live "/api-keys/new", ApiKeyLive.Index, :new
@ -108,27 +111,7 @@ defmodule ZoeyscomputerWeb.Router do
live "/", HomeLive, :index live "/", HomeLive, :index
live "/images/:id", ImageLive.Show, :show live "/images/:id", ImageLive.Show, :show
end live "/gists/:id", GistLive.Show, :show
end
scope "/", ZoeyscomputerWeb do
pipe_through [:browser, :require_authenticated_user]
end
defp download_from_s3(bucket, key) do
case S3.get_object(bucket, key) |> ExAws.request() do
{:ok, %{body: image_binary, headers: headers}} ->
content_type =
Enum.find_value(headers, fn
{"Content-Type", value} -> value
{"content-type", value} -> value
_ -> nil
end)
{:ok, image_binary, content_type || "application/octet-stream"}
error ->
error
end end
end end
end end

View file

@ -0,0 +1,12 @@
defmodule Zoeyscomputer.Repo.Migrations.CreateGists do
use Ecto.Migration
def change do
create table(:gists) do
add :code, :string
add :lang, :string
timestamps(type: :utc_datetime)
end
end
end

View file

@ -0,0 +1,7 @@
defmodule Zoeyscomputer.Repo.Migrations.RemoveLinks do
use Ecto.Migration
def change do
drop table(:links)
end
end

View file

@ -0,0 +1,630 @@
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 100;
font-stretch: normal;
font-style: normal;
src: url('WOFF2/Iosevka-Thin.woff2') format('woff2'), url('TTF/Iosevka-Thin.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 100;
font-stretch: expanded;
font-style: normal;
src: url('WOFF2/Iosevka-ExtendedThin.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThin.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 100;
font-stretch: normal;
font-style: oblique;
src: url('WOFF2/Iosevka-ThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ThinOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 100;
font-stretch: normal;
src: url('WOFF2/Iosevka-ThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ThinOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 100;
font-stretch: expanded;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtendedThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThinOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 100;
font-stretch: expanded;
src: url('WOFF2/Iosevka-ExtendedThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThinOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 100;
font-stretch: normal;
font-style: italic;
src: url('WOFF2/Iosevka-ThinItalic.woff2') format('woff2'), url('TTF/Iosevka-ThinItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 100;
font-stretch: expanded;
font-style: italic;
src: url('WOFF2/Iosevka-ExtendedThinItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThinItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 200;
font-stretch: normal;
font-style: normal;
src: url('WOFF2/Iosevka-ExtraLight.woff2') format('woff2'), url('TTF/Iosevka-ExtraLight.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 200;
font-stretch: expanded;
font-style: normal;
src: url('WOFF2/Iosevka-ExtendedExtraLight.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLight.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 200;
font-stretch: normal;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraLightOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 200;
font-stretch: normal;
src: url('WOFF2/Iosevka-ExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraLightOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 200;
font-stretch: expanded;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtendedExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLightOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 200;
font-stretch: expanded;
src: url('WOFF2/Iosevka-ExtendedExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLightOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 200;
font-stretch: normal;
font-style: italic;
src: url('WOFF2/Iosevka-ExtraLightItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtraLightItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 200;
font-stretch: expanded;
font-style: italic;
src: url('WOFF2/Iosevka-ExtendedExtraLightItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLightItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 300;
font-stretch: normal;
font-style: normal;
src: url('WOFF2/Iosevka-Light.woff2') format('woff2'), url('TTF/Iosevka-Light.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 300;
font-stretch: expanded;
font-style: normal;
src: url('WOFF2/Iosevka-ExtendedLight.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLight.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 300;
font-stretch: normal;
font-style: oblique;
src: url('WOFF2/Iosevka-LightOblique.woff2') format('woff2'), url('TTF/Iosevka-LightOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 300;
font-stretch: normal;
src: url('WOFF2/Iosevka-LightOblique.woff2') format('woff2'), url('TTF/Iosevka-LightOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 300;
font-stretch: expanded;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtendedLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLightOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 300;
font-stretch: expanded;
src: url('WOFF2/Iosevka-ExtendedLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLightOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 300;
font-stretch: normal;
font-style: italic;
src: url('WOFF2/Iosevka-LightItalic.woff2') format('woff2'), url('TTF/Iosevka-LightItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 300;
font-stretch: expanded;
font-style: italic;
src: url('WOFF2/Iosevka-ExtendedLightItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLightItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: normal;
src: url('WOFF2/Iosevka-Regular.woff2') format('woff2'), url('TTF/Iosevka-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 400;
font-stretch: expanded;
font-style: normal;
src: url('WOFF2/Iosevka-Extended.woff2') format('woff2'), url('TTF/Iosevka-Extended.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: oblique;
src: url('WOFF2/Iosevka-Oblique.woff2') format('woff2'), url('TTF/Iosevka-Oblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 400;
font-stretch: normal;
src: url('WOFF2/Iosevka-Oblique.woff2') format('woff2'), url('TTF/Iosevka-Oblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 400;
font-stretch: expanded;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtendedOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 400;
font-stretch: expanded;
src: url('WOFF2/Iosevka-ExtendedOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: italic;
src: url('WOFF2/Iosevka-Italic.woff2') format('woff2'), url('TTF/Iosevka-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 400;
font-stretch: expanded;
font-style: italic;
src: url('WOFF2/Iosevka-ExtendedItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 500;
font-stretch: normal;
font-style: normal;
src: url('WOFF2/Iosevka-Medium.woff2') format('woff2'), url('TTF/Iosevka-Medium.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 500;
font-stretch: expanded;
font-style: normal;
src: url('WOFF2/Iosevka-ExtendedMedium.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMedium.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 500;
font-stretch: normal;
font-style: oblique;
src: url('WOFF2/Iosevka-MediumOblique.woff2') format('woff2'), url('TTF/Iosevka-MediumOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 500;
font-stretch: normal;
src: url('WOFF2/Iosevka-MediumOblique.woff2') format('woff2'), url('TTF/Iosevka-MediumOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 500;
font-stretch: expanded;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtendedMediumOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMediumOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 500;
font-stretch: expanded;
src: url('WOFF2/Iosevka-ExtendedMediumOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMediumOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 500;
font-stretch: normal;
font-style: italic;
src: url('WOFF2/Iosevka-MediumItalic.woff2') format('woff2'), url('TTF/Iosevka-MediumItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 500;
font-stretch: expanded;
font-style: italic;
src: url('WOFF2/Iosevka-ExtendedMediumItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMediumItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 600;
font-stretch: normal;
font-style: normal;
src: url('WOFF2/Iosevka-SemiBold.woff2') format('woff2'), url('TTF/Iosevka-SemiBold.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 600;
font-stretch: expanded;
font-style: normal;
src: url('WOFF2/Iosevka-ExtendedSemiBold.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBold.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 600;
font-stretch: normal;
font-style: oblique;
src: url('WOFF2/Iosevka-SemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-SemiBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 600;
font-stretch: normal;
src: url('WOFF2/Iosevka-SemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-SemiBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 600;
font-stretch: expanded;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtendedSemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 600;
font-stretch: expanded;
src: url('WOFF2/Iosevka-ExtendedSemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 600;
font-stretch: normal;
font-style: italic;
src: url('WOFF2/Iosevka-SemiBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-SemiBoldItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 600;
font-stretch: expanded;
font-style: italic;
src: url('WOFF2/Iosevka-ExtendedSemiBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBoldItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 700;
font-stretch: normal;
font-style: normal;
src: url('WOFF2/Iosevka-Bold.woff2') format('woff2'), url('TTF/Iosevka-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 700;
font-stretch: expanded;
font-style: normal;
src: url('WOFF2/Iosevka-ExtendedBold.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBold.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 700;
font-stretch: normal;
font-style: oblique;
src: url('WOFF2/Iosevka-BoldOblique.woff2') format('woff2'), url('TTF/Iosevka-BoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 700;
font-stretch: normal;
src: url('WOFF2/Iosevka-BoldOblique.woff2') format('woff2'), url('TTF/Iosevka-BoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 700;
font-stretch: expanded;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtendedBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 700;
font-stretch: expanded;
src: url('WOFF2/Iosevka-ExtendedBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 700;
font-stretch: normal;
font-style: italic;
src: url('WOFF2/Iosevka-BoldItalic.woff2') format('woff2'), url('TTF/Iosevka-BoldItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 700;
font-stretch: expanded;
font-style: italic;
src: url('WOFF2/Iosevka-ExtendedBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBoldItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 800;
font-stretch: normal;
font-style: normal;
src: url('WOFF2/Iosevka-ExtraBold.woff2') format('woff2'), url('TTF/Iosevka-ExtraBold.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 800;
font-stretch: expanded;
font-style: normal;
src: url('WOFF2/Iosevka-ExtendedExtraBold.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBold.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 800;
font-stretch: normal;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 800;
font-stretch: normal;
src: url('WOFF2/Iosevka-ExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 800;
font-stretch: expanded;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtendedExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 800;
font-stretch: expanded;
src: url('WOFF2/Iosevka-ExtendedExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBoldOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 800;
font-stretch: normal;
font-style: italic;
src: url('WOFF2/Iosevka-ExtraBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtraBoldItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 800;
font-stretch: expanded;
font-style: italic;
src: url('WOFF2/Iosevka-ExtendedExtraBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBoldItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 900;
font-stretch: normal;
font-style: normal;
src: url('WOFF2/Iosevka-Heavy.woff2') format('woff2'), url('TTF/Iosevka-Heavy.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 900;
font-stretch: expanded;
font-style: normal;
src: url('WOFF2/Iosevka-ExtendedHeavy.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavy.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 900;
font-stretch: normal;
font-style: oblique;
src: url('WOFF2/Iosevka-HeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-HeavyOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 900;
font-stretch: normal;
src: url('WOFF2/Iosevka-HeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-HeavyOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 900;
font-stretch: expanded;
font-style: oblique;
src: url('WOFF2/Iosevka-ExtendedHeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavyOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web Oblique';
font-display: swap;
font-weight: 900;
font-stretch: expanded;
src: url('WOFF2/Iosevka-ExtendedHeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavyOblique.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 900;
font-stretch: normal;
font-style: italic;
src: url('WOFF2/Iosevka-HeavyItalic.woff2') format('woff2'), url('TTF/Iosevka-HeavyItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Iosevka Web';
font-display: swap;
font-weight: 900;
font-stretch: expanded;
font-style: italic;
src: url('WOFF2/Iosevka-ExtendedHeavyItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavyItalic.ttf') format('truetype');
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,21 @@
defmodule Zoeyscomputer.GistsFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Zoeyscomputer.Gists` context.
"""
@doc """
Generate a gist.
"""
def gist_fixture(attrs \\ %{}) do
{:ok, gist} =
attrs
|> Enum.into(%{
code: "some code",
lang: "some lang"
})
|> Zoeyscomputer.Gists.create_gist()
gist
end
end

View file

@ -0,0 +1,61 @@
defmodule Zoeyscomputer.GistsTest do
use Zoeyscomputer.DataCase
alias Zoeyscomputer.Gists
describe "gists" do
alias Zoeyscomputer.Gists.Gist
import Zoeyscomputer.GistsFixtures
@invalid_attrs %{code: nil, lang: nil}
test "list_gists/0 returns all gists" do
gist = gist_fixture()
assert Gists.list_gists() == [gist]
end
test "get_gist!/1 returns the gist with given id" do
gist = gist_fixture()
assert Gists.get_gist!(gist.id) == gist
end
test "create_gist/1 with valid data creates a gist" do
valid_attrs = %{code: "some code", lang: "some lang"}
assert {:ok, %Gist{} = gist} = Gists.create_gist(valid_attrs)
assert gist.code == "some code"
assert gist.lang == "some lang"
end
test "create_gist/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Gists.create_gist(@invalid_attrs)
end
test "update_gist/2 with valid data updates the gist" do
gist = gist_fixture()
update_attrs = %{code: "some updated code", lang: "some updated lang"}
assert {:ok, %Gist{} = gist} = Gists.update_gist(gist, update_attrs)
assert gist.code == "some updated code"
assert gist.lang == "some updated lang"
end
test "update_gist/2 with invalid data returns error changeset" do
gist = gist_fixture()
assert {:error, %Ecto.Changeset{}} = Gists.update_gist(gist, @invalid_attrs)
assert gist == Gists.get_gist!(gist.id)
end
test "delete_gist/1 deletes the gist" do
gist = gist_fixture()
assert {:ok, %Gist{}} = Gists.delete_gist(gist)
assert_raise Ecto.NoResultsError, fn -> Gists.get_gist!(gist.id) end
end
test "change_gist/1 returns a gist changeset" do
gist = gist_fixture()
assert %Ecto.Changeset{} = Gists.change_gist(gist)
end
end
end

View file

@ -0,0 +1,88 @@
defmodule ZoeyscomputerWeb.GistControllerTest do
use ZoeyscomputerWeb.ConnCase
import Zoeyscomputer.GistsFixtures
alias Zoeyscomputer.Gists.Gist
@create_attrs %{
code: "some code",
lang: "some lang"
}
@update_attrs %{
code: "some updated code",
lang: "some updated lang"
}
@invalid_attrs %{code: nil, lang: nil}
setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end
describe "index" do
test "lists all gists", %{conn: conn} do
conn = get(conn, ~p"/api/gists")
assert json_response(conn, 200)["data"] == []
end
end
describe "create gist" do
test "renders gist when data is valid", %{conn: conn} do
conn = post(conn, ~p"/api/gists", gist: @create_attrs)
assert %{"id" => id} = json_response(conn, 201)["data"]
conn = get(conn, ~p"/api/gists/#{id}")
assert %{
"id" => ^id,
"code" => "some code",
"lang" => "some lang"
} = json_response(conn, 200)["data"]
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, ~p"/api/gists", gist: @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "update gist" do
setup [:create_gist]
test "renders gist when data is valid", %{conn: conn, gist: %Gist{id: id} = gist} do
conn = put(conn, ~p"/api/gists/#{gist}", gist: @update_attrs)
assert %{"id" => ^id} = json_response(conn, 200)["data"]
conn = get(conn, ~p"/api/gists/#{id}")
assert %{
"id" => ^id,
"code" => "some updated code",
"lang" => "some updated lang"
} = json_response(conn, 200)["data"]
end
test "renders errors when data is invalid", %{conn: conn, gist: gist} do
conn = put(conn, ~p"/api/gists/#{gist}", gist: @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "delete gist" do
setup [:create_gist]
test "deletes chosen gist", %{conn: conn, gist: gist} do
conn = delete(conn, ~p"/api/gists/#{gist}")
assert response(conn, 204)
assert_error_sent 404, fn ->
get(conn, ~p"/api/gists/#{gist}")
end
end
end
defp create_gist(_) do
gist = gist_fixture()
%{gist: gist}
end
end

View file

@ -0,0 +1,113 @@
defmodule ZoeyscomputerWeb.GistLiveTest do
use ZoeyscomputerWeb.ConnCase
import Phoenix.LiveViewTest
import Zoeyscomputer.GistsFixtures
@create_attrs %{code: "some code", lang: "some lang"}
@update_attrs %{code: "some updated code", lang: "some updated lang"}
@invalid_attrs %{code: nil, lang: nil}
defp create_gist(_) do
gist = gist_fixture()
%{gist: gist}
end
describe "Index" do
setup [:create_gist]
test "lists all gists", %{conn: conn, gist: gist} do
{:ok, _index_live, html} = live(conn, ~p"/gists")
assert html =~ "Listing Gists"
assert html =~ gist.code
end
test "saves new gist", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/gists")
assert index_live |> element("a", "New Gist") |> render_click() =~
"New Gist"
assert_patch(index_live, ~p"/gists/new")
assert index_live
|> form("#gist-form", gist: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#gist-form", gist: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/gists")
html = render(index_live)
assert html =~ "Gist created successfully"
assert html =~ "some code"
end
test "updates gist in listing", %{conn: conn, gist: gist} do
{:ok, index_live, _html} = live(conn, ~p"/gists")
assert index_live |> element("#gists-#{gist.id} a", "Edit") |> render_click() =~
"Edit Gist"
assert_patch(index_live, ~p"/gists/#{gist}/edit")
assert index_live
|> form("#gist-form", gist: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#gist-form", gist: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/gists")
html = render(index_live)
assert html =~ "Gist updated successfully"
assert html =~ "some updated code"
end
test "deletes gist in listing", %{conn: conn, gist: gist} do
{:ok, index_live, _html} = live(conn, ~p"/gists")
assert index_live |> element("#gists-#{gist.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#gists-#{gist.id}")
end
end
describe "Show" do
setup [:create_gist]
test "displays gist", %{conn: conn, gist: gist} do
{:ok, _show_live, html} = live(conn, ~p"/gists/#{gist}")
assert html =~ "Show Gist"
assert html =~ gist.code
end
test "updates gist within modal", %{conn: conn, gist: gist} do
{:ok, show_live, _html} = live(conn, ~p"/gists/#{gist}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Gist"
assert_patch(show_live, ~p"/gists/#{gist}/show/edit")
assert show_live
|> form("#gist-form", gist: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#gist-form", gist: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/gists/#{gist}")
html = render(show_live)
assert html =~ "Gist updated successfully"
assert html =~ "some updated code"
end
end
end