feat: added admin web page 19a6660f
Steve · 2026-03-04 23:46 3 file(s) · +146 −0
src/server.rs +9 −0
63 63
struct IndexTemplate;
64 64
65 65
#[derive(Template)]
66 +
#[template(path = "admin.html")]
67 +
struct AdminTemplate;
68 +
69 +
#[derive(Template)]
66 70
#[template(path = "snippet.html")]
67 71
struct SnippetTemplate {
68 72
    name: String,
78 82
79 83
async fn index() -> WebTemplate<IndexTemplate> {
80 84
    WebTemplate(IndexTemplate)
85 +
}
86 +
87 +
async fn admin() -> WebTemplate<AdminTemplate> {
88 +
    WebTemplate(AdminTemplate)
81 89
}
82 90
83 91
fn is_cli_user_agent(headers: &HeaderMap) -> bool {
376 384
377 385
    let app = Router::new()
378 386
        .route("/", get(index))
387 +
        .route("/admin", get(admin))
379 388
        .route("/s/{short_id}", get(view_snippet))
380 389
        .route("/snippets", post(create_snippet))
381 390
        .merge(api_routes)
static/styles.css +31 −0
76 76
	padding: 4px;
77 77
}
78 78
79 +
#authForm input {
80 +
	background: #121113;
81 +
	color: #ffffff;
82 +
	border: 1px solid white;
83 +
	padding: 4px;
84 +
}
85 +
79 86
textarea {
80 87
	background: #121113;
81 88
	color: #ffffff;
119 126
		padding: 1rem;
120 127
		gap: 1rem;
121 128
	}
129 +
}
130 +
131 +
#snippetList {
132 +
	flex-direction: column;
133 +
	gap: 0;
134 +
}
135 +
136 +
.snippet-item {
137 +
	display: flex;
138 +
	justify-content: space-between;
139 +
	align-items: center;
140 +
	padding: 8px;
141 +
	border: 1px solid white;
142 +
	margin-top: -1px;
143 +
	text-decoration: none;
144 +
}
145 +
146 +
.snippet-item:hover {
147 +
	background: #1e1d1f;
148 +
}
149 +
150 +
.snippet-id {
151 +
	color: #878787;
152 +
	font-size: 13px;
122 153
}
123 154
124 155
@font-face {
templates/admin.html (added) +106 −0
1 +
<!doctype html>
2 +
<html lang="en">
3 +
  <head>
4 +
    <meta charset="UTF-8" />
5 +
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 +
    <meta name="theme-color" content="#121113" />
7 +
    <link rel="stylesheet" href="/static/styles.css" />
8 +
    <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
9 +
    <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
10 +
    <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
11 +
    <link rel="manifest" href="/assets/site.webmanifest">
12 +
13 +
    <title>Sipp - Admin</title>
14 +
    <meta name="description" content="Minimal Code Sharing">
15 +
16 +
    <meta property="og:url" content="https://sipp.so">
17 +
    <meta property="og:type" content="website">
18 +
    <meta property="og:title" content="Sipps">
19 +
    <meta property="og:description" content="Minimal Code Sharing">
20 +
    <meta property="og:image" content="https://sipp.so/assets/og.png">
21 +
22 +
    <meta name="twitter:card" content="summary_large_image">
23 +
    <meta property="twitter:domain" content="sipp.so">
24 +
    <meta property="twitter:url" content="https://sipp.so">
25 +
    <meta name="twitter:title" content="Sipps">
26 +
    <meta name="twitter:description" content="Minimal Code Sharing">
27 +
    <meta name="twitter:image" content="https://sipp.so/assets/og.png">
28 +
  </head>
29 +
  <body>
30 +
31 +
    <div class="nav">
32 +
      <a href="/" class="header">
33 +
        <h1>SIPP</h1>
34 +
      </a>
35 +
36 +
      <a class="icon" target="_blank" href="https://github.com/stevedylandev/sipp">
37 +
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
38 +
          <title>GitHub</title>
39 +
          <path d="m21.838 11.677l-9.549-9.58c-.129-.13-.451-.13-.645 0L9 4.742l2.452 2.452c.193-.097.419-.13.645-.13c.903 0 1.58.742 1.58 1.581c0 .226-.032.452-.129.645l1.968 1.968c.194-.097.42-.129.645-.129c.904 0 1.58.742 1.58 1.58c0 .904-.741 1.581-1.58 1.581c-.903 0-1.58-.742-1.58-1.58c0-.226.032-.452.129-.646l-1.968-1.967h-.032v3.71c.58.258 1 .806 1 1.483c0 .904-.742 1.581-1.581 1.581c-.903 0-1.58-.742-1.58-1.58c0-.678.419-1.259 1-1.485v-3.612c-.581-.259-1-.807-1-1.484c0-.226.032-.452.128-.645L8.225 5.613l-6.097 6.064c-.129.13-.129.452 0 .646l9.58 9.58c.13.13.452.13.646 0l9.548-9.58a.59.59 0 0 0-.064-.646"/>
40 +
        </svg>
41 +
      </a>
42 +
    </div>
43 +
44 +
    <div id="authForm" style="display: flex; gap: 1rem; width: 100%;">
45 +
      <input placeholder="API Key" type="password" id="apiKey" style="flex: 1;">
46 +
      <button id="loadBtn" onclick="loadSnippets()">Load Snippets</button>
47 +
    </div>
48 +
49 +
    <div id="error" style="display: none; color: #ff6b6b;"></div>
50 +
51 +
    <div id="snippetList" style="display: none; width: 100%;"></div>
52 +
53 +
    <script>
54 +
      async function loadSnippets() {
55 +
        const apiKey = document.getElementById('apiKey').value;
56 +
        const errorEl = document.getElementById('error');
57 +
        const listEl = document.getElementById('snippetList');
58 +
        const loadBtn = document.getElementById('loadBtn');
59 +
60 +
        errorEl.style.display = 'none';
61 +
        listEl.style.display = 'none';
62 +
        loadBtn.textContent = 'Loading...';
63 +
        loadBtn.disabled = true;
64 +
65 +
        try {
66 +
          const res = await fetch('/api/snippets', {
67 +
            headers: { 'x-api-key': apiKey }
68 +
          });
69 +
70 +
          if (!res.ok) {
71 +
            const data = await res.json();
72 +
            throw new Error(data.error || 'Failed to load snippets');
73 +
          }
74 +
75 +
          const snippets = await res.json();
76 +
77 +
          if (snippets.length === 0) {
78 +
            listEl.innerHTML = '<p>No snippets found.</p>';
79 +
          } else {
80 +
            listEl.innerHTML = snippets.map(s =>
81 +
              `<a class="snippet-item" href="/s/${s.short_id}">` +
82 +
                `<span class="snippet-name">${s.name}</span>` +
83 +
                `<span class="snippet-id">/s/${s.short_id}</span>` +
84 +
              `</a>`
85 +
            ).join('');
86 +
          }
87 +
88 +
          listEl.style.display = 'flex';
89 +
        } catch (err) {
90 +
          errorEl.textContent = err.message;
91 +
          errorEl.style.display = 'block';
92 +
        } finally {
93 +
          loadBtn.textContent = 'Load Snippets';
94 +
          loadBtn.disabled = false;
95 +
        }
96 +
      }
97 +
98 +
      document.getElementById('apiKey').addEventListener('keydown', (e) => {
99 +
        if (e.key === 'Enter') {
100 +
          e.preventDefault();
101 +
          loadSnippets();
102 +
        }
103 +
      });
104 +
    </script>
105 +
  </body>
106 +
</html>