Compare commits
7 Commits
4378a6fb21
...
7e0d7d8888
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e0d7d8888 | |||
| 028d83b9cc | |||
| db57ac843b | |||
| 830ee36f84 | |||
| 6152dcdeea | |||
| 63d91043e4 | |||
| 11bb3ddd1c |
@@ -127,8 +127,8 @@ html, body {
|
|||||||
[data-theme=dark] {
|
[data-theme=dark] {
|
||||||
--mx-fg2: #9090a8;
|
--mx-fg2: #9090a8;
|
||||||
--mx-muted: #5a5a72;
|
--mx-muted: #5a5a72;
|
||||||
--mx-border: #1e1e26;
|
--mx-border: oklch(22% 0.010 270);
|
||||||
--mx-border2: #2a2a36;
|
--mx-border2: oklch(30% 0.012 270);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=light] {
|
[data-theme=light] {
|
||||||
@@ -169,7 +169,7 @@ html, body {
|
|||||||
padding: 1.5rem 1rem;
|
padding: 1.5rem 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-right: 1px solid var(--mx-border);
|
border-right: 1px solid color-mix(in oklch, var(--color-base-content) 18%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx-logo {
|
.mx-logo {
|
||||||
@@ -204,13 +204,23 @@ html, body {
|
|||||||
border-radius: var(--mx-radius-sm);
|
border-radius: var(--mx-radius-sm);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--mx-fg2);
|
color: var(--mx-fg2);
|
||||||
font-size: 0.9375rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: color 0.15s, background 0.15s;
|
cursor: pointer;
|
||||||
|
transition: color 0.15s, background 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx-nav-item:hover { color: var(--mx-fg); background: var(--mx-surface2); }
|
.mx-nav-item:hover {
|
||||||
.mx-nav-active { color: var(--mx-fg) !important; }
|
color: var(--mx-fg);
|
||||||
|
background: var(--mx-surface2);
|
||||||
|
box-shadow: inset 3px 0 0 var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-nav-active {
|
||||||
|
color: var(--mx-fg) !important;
|
||||||
|
background: color-mix(in oklch, var(--color-primary) 12%, transparent) !important;
|
||||||
|
box-shadow: inset 3px 0 0 var(--color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.mx-sidebar-footer {
|
.mx-sidebar-footer {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
@@ -236,7 +246,7 @@ html, body {
|
|||||||
|
|
||||||
/* ── Main ── */
|
/* ── Main ── */
|
||||||
.mx-main {
|
.mx-main {
|
||||||
border-right: 1px solid var(--mx-border);
|
border-right: 1px solid color-mix(in oklch, var(--color-base-content) 18%, transparent);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,10 +287,10 @@ html, body {
|
|||||||
}
|
}
|
||||||
.mx-refresh-btn:hover { color: var(--mx-fg); border-color: var(--mx-accent); }
|
.mx-refresh-btn:hover { color: var(--mx-fg); border-color: var(--mx-accent); }
|
||||||
|
|
||||||
.mx-divider { height: 1px; background: var(--mx-border); }
|
.mx-divider { height: 1px; background: color-mix(in oklch, var(--color-base-content) 18%, transparent); }
|
||||||
|
|
||||||
/* ── Compose ── */
|
/* ── Compose ── */
|
||||||
.mx-compose-wrapper { padding: 1rem 1.25rem; border-bottom: 1px solid var(--mx-border); }
|
.mx-compose-wrapper { padding: 1rem 1.25rem; }
|
||||||
|
|
||||||
.mx-compose {
|
.mx-compose {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -311,7 +321,7 @@ html, body {
|
|||||||
outline: none;
|
outline: none;
|
||||||
color: var(--mx-fg);
|
color: var(--mx-fg);
|
||||||
font-family: 'Geist', system-ui, sans-serif;
|
font-family: 'Geist', system-ui, sans-serif;
|
||||||
font-size: 1rem;
|
font-size: 1.0625rem;
|
||||||
resize: none;
|
resize: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
@@ -360,16 +370,20 @@ html, body {
|
|||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s, opacity 0.15s;
|
transition: background 0.15s, opacity 0.15s, box-shadow 0.15s;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in oklch, var(--color-primary) 60%, transparent);
|
||||||
|
}
|
||||||
|
.mx-btn-post:hover, .mx-btn-save:hover {
|
||||||
|
background: var(--mx-accent2);
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in oklch, var(--color-accent) 80%, transparent);
|
||||||
}
|
}
|
||||||
.mx-btn-post:hover, .mx-btn-save:hover { background: var(--mx-accent2); }
|
|
||||||
.mx-btn-post:disabled, .mx-btn-save:disabled { opacity: 0.5; cursor: not-allowed; }
|
.mx-btn-post:disabled, .mx-btn-save:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
.mx-btn-cancel {
|
.mx-btn-cancel {
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid var(--mx-border2);
|
border: 1px solid var(--mx-fg2);
|
||||||
color: var(--mx-fg2);
|
color: var(--mx-fg2);
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
padding: 0.375rem 0.875rem;
|
padding: 0.375rem 0.875rem;
|
||||||
@@ -380,16 +394,26 @@ html, body {
|
|||||||
}
|
}
|
||||||
.mx-btn-cancel:hover { color: var(--mx-fg); border-color: var(--mx-fg2); }
|
.mx-btn-cancel:hover { color: var(--mx-fg); border-color: var(--mx-fg2); }
|
||||||
|
|
||||||
|
/* ── Feed ── */
|
||||||
|
.mx-feed {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Tweet Card ── */
|
/* ── Tweet Card ── */
|
||||||
.mx-tweet {
|
.mx-tweet {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
border-bottom: 1px solid var(--mx-border);
|
border: 1px solid var(--mx-border);
|
||||||
transition: background 0.1s;
|
border-radius: var(--mx-radius);
|
||||||
|
background: var(--mx-surface);
|
||||||
|
transition: background 0.1s, border-color 0.1s;
|
||||||
animation: mx-fade-in 0.2s ease;
|
animation: mx-fade-in 0.2s ease;
|
||||||
}
|
}
|
||||||
.mx-tweet:hover { background: color-mix(in oklch, var(--mx-fg) 2%, transparent); }
|
.mx-tweet:hover { background: var(--mx-surface2); border-color: var(--mx-border2); }
|
||||||
|
|
||||||
@keyframes mx-fade-in {
|
@keyframes mx-fade-in {
|
||||||
from { opacity: 0; transform: translateY(4px); }
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
@@ -406,7 +430,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx-tweet-handle {
|
.mx-tweet-handle {
|
||||||
font-size: 0.875rem;
|
font-size: 0.9375rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--mx-fg);
|
color: var(--mx-fg);
|
||||||
}
|
}
|
||||||
@@ -420,7 +444,7 @@ html, body {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.125rem;
|
gap: 0.125rem;
|
||||||
opacity: 0;
|
opacity: 0.25;
|
||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
.mx-tweet:hover .mx-tweet-actions { opacity: 1; }
|
.mx-tweet:hover .mx-tweet-actions { opacity: 1; }
|
||||||
@@ -443,7 +467,7 @@ html, body {
|
|||||||
.mx-action-confirm { color: var(--mx-red) !important; background: color-mix(in oklch, var(--mx-red) 15%, transparent) !important; }
|
.mx-action-confirm { color: var(--mx-red) !important; background: color-mix(in oklch, var(--mx-red) 15%, transparent) !important; }
|
||||||
|
|
||||||
.mx-tweet-text {
|
.mx-tweet-text {
|
||||||
font-size: 0.9375rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--mx-fg);
|
color: var(--mx-fg);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Do not edit this file manually
|
// Do not edit this file manually
|
||||||
|
|
||||||
|
|
||||||
import type { AshRpcError, ConditionalPaginatedResultMixed, InferResult, SortString, UUID, UnifiedFieldSelection, ValidationResult, tweetsFilterInput, tweetsResourceSchema, tweetsSortField } from "./ash_types";
|
import type { AshRpcError, ConditionalPaginatedResultMixed, InferResult, SortString, UUID, UnifiedFieldSelection, ValidationResult, mediaFilterInput, mediaResourceSchema, mediaSortField, tweetsFilterInput, tweetsResourceSchema, tweetsSortField } from "./ash_types";
|
||||||
export type * from "./ash_types";
|
export type * from "./ash_types";
|
||||||
|
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
@@ -201,8 +201,110 @@ export async function executeValidationRpcRequest<T>(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type ReadMediaFields = UnifiedFieldSelection<mediaResourceSchema>[];
|
||||||
|
|
||||||
|
|
||||||
|
export type InferReadMediaResult<
|
||||||
|
Fields extends ReadMediaFields | undefined,
|
||||||
|
Page extends ReadMediaConfig["page"] = undefined
|
||||||
|
> = ConditionalPaginatedResultMixed<Page, Array<InferResult<mediaResourceSchema, Fields>>, {
|
||||||
|
results: Array<InferResult<mediaResourceSchema, Fields>>;
|
||||||
|
hasMore: boolean;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
count?: number | null;
|
||||||
|
type: "offset";
|
||||||
|
}, {
|
||||||
|
results: Array<InferResult<mediaResourceSchema, Fields>>;
|
||||||
|
hasMore: boolean;
|
||||||
|
limit: number;
|
||||||
|
after: string | null;
|
||||||
|
before: string | null;
|
||||||
|
previousPage: string;
|
||||||
|
nextPage: string;
|
||||||
|
count?: number | null;
|
||||||
|
type: "keyset";
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type ReadMediaConfig = {
|
||||||
|
tenant?: string;
|
||||||
|
fields: ReadMediaFields;
|
||||||
|
filter?: mediaFilterInput;
|
||||||
|
sort?: SortString<mediaSortField> | SortString<mediaSortField>[];
|
||||||
|
page?: (
|
||||||
|
{
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
count?: boolean;
|
||||||
|
} | {
|
||||||
|
limit?: number;
|
||||||
|
after?: string;
|
||||||
|
before?: string;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
fetchOptions?: RequestInit;
|
||||||
|
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReadMediaResult<Fields extends ReadMediaFields, Page extends ReadMediaConfig["page"] = undefined> = | { success: true; data: InferReadMediaResult<Fields, Page>; }
|
||||||
|
| { success: false; errors: AshRpcError[]; }
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read Media records
|
||||||
|
*
|
||||||
|
* @ashActionType :read
|
||||||
|
*/
|
||||||
|
export async function readMedia<Fields extends ReadMediaFields, Config extends ReadMediaConfig = ReadMediaConfig>(
|
||||||
|
config: Config & { fields: Fields }
|
||||||
|
): Promise<ReadMediaResult<Fields, Config["page"]>> {
|
||||||
|
const payload = {
|
||||||
|
action: "read_media",
|
||||||
|
...(config.tenant !== undefined && { tenant: config.tenant }),
|
||||||
|
...(config.fields !== undefined && { fields: config.fields }),
|
||||||
|
...(config.filter && { filter: config.filter }),
|
||||||
|
...(config.sort && { sort: Array.isArray(config.sort) ? config.sort.join(",") : config.sort }),
|
||||||
|
...(config.page && { page: config.page })
|
||||||
|
};
|
||||||
|
|
||||||
|
return executeActionRpcRequest<ReadMediaResult<Fields, Config["page"]>>(
|
||||||
|
payload,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate: Read Media records
|
||||||
|
*
|
||||||
|
* @ashActionType :read
|
||||||
|
* @validation true
|
||||||
|
*/
|
||||||
|
export async function validateReadMedia(
|
||||||
|
config: {
|
||||||
|
tenant?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
fetchOptions?: RequestInit;
|
||||||
|
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||||
|
}
|
||||||
|
): Promise<ValidationResult> {
|
||||||
|
const payload = {
|
||||||
|
action: "read_media",
|
||||||
|
...(config.tenant !== undefined && { tenant: config.tenant })
|
||||||
|
};
|
||||||
|
|
||||||
|
return executeValidationRpcRequest<ValidationResult>(
|
||||||
|
payload,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type CreateTweetInput = {
|
export type CreateTweetInput = {
|
||||||
content: string;
|
content: string;
|
||||||
|
mediaId?: UUID;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateTweetFields = UnifiedFieldSelection<tweetsResourceSchema>[];
|
export type CreateTweetFields = UnifiedFieldSelection<tweetsResourceSchema>[];
|
||||||
@@ -436,6 +538,7 @@ export async function validateReadTweet(
|
|||||||
|
|
||||||
export type UpdateTweetInput = {
|
export type UpdateTweetInput = {
|
||||||
content?: string;
|
content?: string;
|
||||||
|
likes?: number;
|
||||||
userId?: UUID;
|
userId?: UUID;
|
||||||
state?: "posted" | "drafted";
|
state?: "posted" | "drafted";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,28 +5,88 @@
|
|||||||
|
|
||||||
export type UUID = string;
|
export type UUID = string;
|
||||||
|
|
||||||
|
// media Schema
|
||||||
|
export type mediaResourceSchema = {
|
||||||
|
__type: "Resource";
|
||||||
|
__primitiveFields: "id" | "s3Key" | "userId" | "tweetId";
|
||||||
|
id: UUID;
|
||||||
|
s3Key: string;
|
||||||
|
userId: UUID;
|
||||||
|
tweetId: UUID | null;
|
||||||
|
tweet: { __type: "Relationship"; __resource: tweetsResourceSchema | null; };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type mediaAttributesOnlySchema = {
|
||||||
|
__type: "Resource";
|
||||||
|
__primitiveFields: "id" | "s3Key" | "userId" | "tweetId";
|
||||||
|
id: UUID;
|
||||||
|
s3Key: string;
|
||||||
|
userId: UUID;
|
||||||
|
tweetId: UUID | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// tweets Schema
|
// tweets Schema
|
||||||
export type tweetsResourceSchema = {
|
export type tweetsResourceSchema = {
|
||||||
__type: "Resource";
|
__type: "Resource";
|
||||||
__primitiveFields: "id" | "content" | "userId" | "state";
|
__primitiveFields: "id" | "content" | "likes" | "userId" | "state";
|
||||||
id: UUID;
|
id: UUID;
|
||||||
content: string;
|
content: string;
|
||||||
|
likes: number;
|
||||||
userId: UUID;
|
userId: UUID;
|
||||||
state: "posted" | "drafted";
|
state: "posted" | "drafted";
|
||||||
|
media: { __type: "Relationship"; __array: true; __resource: mediaResourceSchema; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type tweetsAttributesOnlySchema = {
|
export type tweetsAttributesOnlySchema = {
|
||||||
__type: "Resource";
|
__type: "Resource";
|
||||||
__primitiveFields: "id" | "content" | "userId" | "state";
|
__primitiveFields: "id" | "content" | "likes" | "userId" | "state";
|
||||||
id: UUID;
|
id: UUID;
|
||||||
content: string;
|
content: string;
|
||||||
|
likes: number;
|
||||||
userId: UUID;
|
userId: UUID;
|
||||||
state: "posted" | "drafted";
|
state: "posted" | "drafted";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type mediaFilterInput = {
|
||||||
|
and?: Array<mediaFilterInput>;
|
||||||
|
or?: Array<mediaFilterInput>;
|
||||||
|
not?: Array<mediaFilterInput>;
|
||||||
|
|
||||||
|
id?: {
|
||||||
|
eq?: UUID;
|
||||||
|
notEq?: UUID;
|
||||||
|
in?: Array<UUID>;
|
||||||
|
};
|
||||||
|
|
||||||
|
s3Key?: {
|
||||||
|
eq?: string;
|
||||||
|
notEq?: string;
|
||||||
|
in?: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
userId?: {
|
||||||
|
eq?: UUID;
|
||||||
|
notEq?: UUID;
|
||||||
|
in?: Array<UUID>;
|
||||||
|
};
|
||||||
|
|
||||||
|
tweetId?: {
|
||||||
|
eq?: UUID;
|
||||||
|
notEq?: UUID;
|
||||||
|
in?: Array<UUID>;
|
||||||
|
isNil?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
tweet?: tweetsFilterInput;
|
||||||
|
|
||||||
|
};
|
||||||
export type tweetsFilterInput = {
|
export type tweetsFilterInput = {
|
||||||
and?: Array<tweetsFilterInput>;
|
and?: Array<tweetsFilterInput>;
|
||||||
or?: Array<tweetsFilterInput>;
|
or?: Array<tweetsFilterInput>;
|
||||||
@@ -44,6 +104,16 @@ export type tweetsFilterInput = {
|
|||||||
in?: Array<string>;
|
in?: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
likes?: {
|
||||||
|
eq?: number;
|
||||||
|
notEq?: number;
|
||||||
|
greaterThan?: number;
|
||||||
|
greaterThanOrEqual?: number;
|
||||||
|
lessThan?: number;
|
||||||
|
lessThanOrEqual?: number;
|
||||||
|
in?: Array<number>;
|
||||||
|
};
|
||||||
|
|
||||||
userId?: {
|
userId?: {
|
||||||
eq?: UUID;
|
eq?: UUID;
|
||||||
notEq?: UUID;
|
notEq?: UUID;
|
||||||
@@ -57,15 +127,22 @@ export type tweetsFilterInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
media?: mediaFilterInput;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const tweetsFilterFields = ["id", "content", "userId", "state", "user"] as const;
|
export const mediaFilterFields = ["id", "s3Key", "userId", "tweetId", "user", "tweet"] as const;
|
||||||
|
export type mediaFilterField = (typeof mediaFilterFields)[number];
|
||||||
|
|
||||||
|
export const tweetsFilterFields = ["id", "content", "likes", "userId", "state", "user", "media"] as const;
|
||||||
export type tweetsFilterField = (typeof tweetsFilterFields)[number];
|
export type tweetsFilterField = (typeof tweetsFilterFields)[number];
|
||||||
|
|
||||||
|
|
||||||
export const tweetsSortFields = ["id", "content", "userId", "state"] as const;
|
export const mediaSortFields = ["id", "s3Key", "userId", "tweetId"] as const;
|
||||||
|
export type mediaSortField = (typeof mediaSortFields)[number];
|
||||||
|
|
||||||
|
export const tweetsSortFields = ["id", "content", "likes", "userId", "state"] as const;
|
||||||
export type tweetsSortField = (typeof tweetsSortFields)[number];
|
export type tweetsSortField = (typeof tweetsSortFields)[number];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
updateTweet,
|
updateTweet,
|
||||||
buildCSRFHeaders,
|
buildCSRFHeaders,
|
||||||
} from "./ash_rpc";
|
} from "./ash_rpc";
|
||||||
|
import { uploadFile } from "./upload";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: { queries: { staleTime: 10_000 } },
|
defaultOptions: { queries: { staleTime: 10_000 } },
|
||||||
@@ -21,7 +22,8 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type Tweet = { id: string; content: string; userId: string; state: string };
|
type MediaItem = { id: string; s3Key: string };
|
||||||
|
type Tweet = { id: string; content: string; userId: string; state: string; media?: MediaItem[] };
|
||||||
|
|
||||||
// ── Auth context ───────────────────────────────────────────────────────────────
|
// ── Auth context ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -33,6 +35,11 @@ function timeAgo(): string {
|
|||||||
return "just now";
|
return "just now";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAssetHost(): string {
|
||||||
|
const appEl = document.getElementById("app");
|
||||||
|
return appEl?.dataset.assetHost ?? "http://localhost:9000";
|
||||||
|
}
|
||||||
|
|
||||||
// ── Components ─────────────────────────────────────────────────────────────────
|
// ── Components ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function Spinner() {
|
function Spinner() {
|
||||||
@@ -67,14 +74,20 @@ function CharCount({ current, max }: { current: number; max: number }) {
|
|||||||
function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [pendingFile, setPendingFile] = useState<File | null>(null);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
const [mediaId, setMediaId] = useState<string | null>(null);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const MAX = 280;
|
const MAX = 280;
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (content: string) => {
|
mutationFn: async (content: string) => {
|
||||||
const res = await createTweet({
|
const res = await createTweet({
|
||||||
input: { content },
|
input: { content, mediaId: mediaId ?? undefined },
|
||||||
fields: ["id", "content", "userId", "state"],
|
fields: ["id", "content", "userId", "state"],
|
||||||
headers: buildCSRFHeaders(),
|
headers: buildCSRFHeaders(),
|
||||||
});
|
});
|
||||||
@@ -85,11 +98,52 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
|||||||
qc.invalidateQueries({ queryKey: ["tweets"] });
|
qc.invalidateQueries({ queryKey: ["tweets"] });
|
||||||
setText("");
|
setText("");
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setMediaId(null);
|
||||||
|
setPendingFile(null);
|
||||||
|
setUploadError(null);
|
||||||
|
if (previewUrl) {
|
||||||
|
URL.revokeObjectURL(previewUrl);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
}
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
},
|
},
|
||||||
onError: (e: Error) => setError(e.message),
|
onError: (e: Error) => setError(e.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
// Reset the input so the same file can be re-selected after removal
|
||||||
|
e.target.value = "";
|
||||||
|
// Revoke any previous object URL to avoid memory leaks
|
||||||
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||||
|
const localUrl = URL.createObjectURL(file);
|
||||||
|
setPendingFile(file);
|
||||||
|
setPreviewUrl(localUrl);
|
||||||
|
setMediaId(null);
|
||||||
|
setUploadError(null);
|
||||||
|
setUploading(true);
|
||||||
|
const csrfToken = buildCSRFHeaders()["X-CSRF-Token"] as string;
|
||||||
|
const result = await uploadFile(file, csrfToken);
|
||||||
|
setUploading(false);
|
||||||
|
if ("error" in result) {
|
||||||
|
setUploadError(result.error);
|
||||||
|
setPendingFile(null);
|
||||||
|
URL.revokeObjectURL(localUrl);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
} else {
|
||||||
|
setMediaId(result.mediaId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAttachment() {
|
||||||
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||||
|
setPendingFile(null);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
setMediaId(null);
|
||||||
|
setUploadError(null);
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -132,15 +186,80 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
|||||||
rows={2}
|
rows={2}
|
||||||
maxLength={MAX + 1}
|
maxLength={MAX + 1}
|
||||||
/>
|
/>
|
||||||
|
{previewUrl && pendingFile && (
|
||||||
|
<div style={{ position: "relative", marginTop: "0.5rem", display: "inline-block" }}>
|
||||||
|
{/\.(mp4|mov)$/i.test(pendingFile.name) ? (
|
||||||
|
<video
|
||||||
|
src={previewUrl}
|
||||||
|
controls
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "200px", borderRadius: "0.5rem", display: "block" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt="attachment preview"
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "200px", borderRadius: "0.5rem", display: "block" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{uploading && (
|
||||||
|
<div style={{
|
||||||
|
position: "absolute", inset: 0, background: "rgba(0,0,0,0.4)",
|
||||||
|
borderRadius: "0.5rem", display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
color: "#fff", fontSize: "0.75rem"
|
||||||
|
}}>
|
||||||
|
Uploading…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={removeAttachment}
|
||||||
|
style={{
|
||||||
|
position: "absolute", top: "4px", right: "4px",
|
||||||
|
background: "rgba(0,0,0,0.6)", border: "none", borderRadius: "50%",
|
||||||
|
width: "20px", height: "20px", cursor: "pointer",
|
||||||
|
color: "#fff", fontSize: "12px", lineHeight: 1,
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center"
|
||||||
|
}}
|
||||||
|
title="Remove attachment"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploadError && <p className="mx-compose-error">{uploadError}</p>}
|
||||||
{error && <p className="mx-compose-error">{error}</p>}
|
{error && <p className="mx-compose-error">{error}</p>}
|
||||||
<div className="mx-compose-footer">
|
<div className="mx-compose-footer">
|
||||||
<span className="mx-compose-hint">⌘↵ to post</span>
|
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mx-action-btn"
|
||||||
|
title="Attach image or video"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={uploading || mutation.isPending}
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*,video/mp4,video/quicktime"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
{uploading && (
|
||||||
|
<span style={{ fontSize: "0.75rem", color: "var(--mx-muted)" }}>
|
||||||
|
{pendingFile?.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="mx-compose-actions">
|
<div className="mx-compose-actions">
|
||||||
<CharCount current={text.length} max={MAX} />
|
<CharCount current={text.length} max={MAX} />
|
||||||
<button
|
<button
|
||||||
className="mx-btn-post"
|
className="mx-btn-post"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
disabled={!text.trim() || mutation.isPending}
|
disabled={!text.trim() || mutation.isPending || uploading}
|
||||||
>
|
>
|
||||||
{mutation.isPending ? "Posting…" : "Post"}
|
{mutation.isPending ? "Posting…" : "Post"}
|
||||||
</button>
|
</button>
|
||||||
@@ -151,6 +270,31 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TweetMedia({ media }: { media: MediaItem[] }) {
|
||||||
|
const assetHost = getAssetHost();
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: "0.5rem", display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||||
|
{media.map((m) =>
|
||||||
|
/\.(mp4|mov)$/i.test(m.s3Key) ? (
|
||||||
|
<video
|
||||||
|
key={m.id}
|
||||||
|
src={`${assetHost}/${m.s3Key}`}
|
||||||
|
controls
|
||||||
|
style={{ maxWidth: "100%", borderRadius: "0.5rem" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
key={m.id}
|
||||||
|
src={`${assetHost}/${m.s3Key}`}
|
||||||
|
alt=""
|
||||||
|
style={{ maxWidth: "100%", borderRadius: "0.5rem", display: "block" }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TweetCard({ tweet }: { tweet: Tweet }) {
|
function TweetCard({ tweet }: { tweet: Tweet }) {
|
||||||
const { userId: currentUserId } = useContext(AuthCtx);
|
const { userId: currentUserId } = useContext(AuthCtx);
|
||||||
const canModify = !!currentUserId && tweet.userId === currentUserId;
|
const canModify = !!currentUserId && tweet.userId === currentUserId;
|
||||||
@@ -283,6 +427,10 @@ function TweetCard({ tweet }: { tweet: Tweet }) {
|
|||||||
<p className="mx-tweet-text">{tweet.content}</p>
|
<p className="mx-tweet-text">{tweet.content}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{tweet.media && tweet.media.length > 0 && (
|
||||||
|
<TweetMedia media={tweet.media} />
|
||||||
|
)}
|
||||||
|
|
||||||
{error && !editing && <p className="mx-compose-error">{error}</p>}
|
{error && !editing && <p className="mx-compose-error">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -294,7 +442,7 @@ function Feed() {
|
|||||||
queryKey: ["tweets"],
|
queryKey: ["tweets"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await readTweet({
|
const res = await readTweet({
|
||||||
fields: ["id", "content", "userId", "state"],
|
fields: ["id", "content", "userId", "state", { media: ["id", "s3Key"] }],
|
||||||
sort: "-id",
|
sort: "-id",
|
||||||
headers: buildCSRFHeaders(),
|
headers: buildCSRFHeaders(),
|
||||||
});
|
});
|
||||||
|
|||||||
29
assets/js/upload.ts
Normal file
29
assets/js/upload.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export interface UploadResult {
|
||||||
|
success: true;
|
||||||
|
mediaId: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadError {
|
||||||
|
success?: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFile(
|
||||||
|
file: File,
|
||||||
|
csrfToken: string
|
||||||
|
): Promise<UploadResult | UploadError> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
// Do NOT set Content-Type — browser sets the multipart boundary automatically
|
||||||
|
const res = await fetch("/upload", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "X-CSRF-Token": csrfToken },
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (!res.ok || !json.success) {
|
||||||
|
return { error: json.error ?? "Upload failed" };
|
||||||
|
}
|
||||||
|
return json as UploadResult;
|
||||||
|
}
|
||||||
@@ -7,6 +7,14 @@
|
|||||||
# General application configuration
|
# General application configuration
|
||||||
import Config
|
import Config
|
||||||
|
|
||||||
|
config :waffle,
|
||||||
|
storage: Waffle.Storage.S3,
|
||||||
|
bucket: "mixer-bucket",
|
||||||
|
asset_host: "http://localhost:9000"
|
||||||
|
|
||||||
|
config :ex_aws,
|
||||||
|
json_codec: Jason
|
||||||
|
|
||||||
config :ash_typescript,
|
config :ash_typescript,
|
||||||
output_file: "assets/js/ash_rpc.ts",
|
output_file: "assets/js/ash_rpc.ts",
|
||||||
run_endpoint: "/rpc/run",
|
run_endpoint: "/rpc/run",
|
||||||
|
|||||||
@@ -91,3 +91,14 @@ config :phoenix_live_view,
|
|||||||
|
|
||||||
# Disable swoosh api client as it is only required for production adapters.
|
# Disable swoosh api client as it is only required for production adapters.
|
||||||
config :swoosh, :api_client, false
|
config :swoosh, :api_client, false
|
||||||
|
|
||||||
|
# Local S3-compatible storage (MinIO at localhost:9000)
|
||||||
|
config :ex_aws,
|
||||||
|
access_key_id: "minioadmin",
|
||||||
|
secret_access_key: "minioadmin"
|
||||||
|
|
||||||
|
config :ex_aws, :s3,
|
||||||
|
scheme: "http://",
|
||||||
|
host: "localhost",
|
||||||
|
port: 9000,
|
||||||
|
virtual_host: false
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ defmodule Mixer.Posts do
|
|||||||
|
|
||||||
resources do
|
resources do
|
||||||
resource Mixer.Posts.Tweet
|
resource Mixer.Posts.Tweet
|
||||||
|
resource Mixer.Posts.Media
|
||||||
end
|
end
|
||||||
|
|
||||||
typescript_rpc do
|
typescript_rpc do
|
||||||
@@ -18,5 +19,9 @@ defmodule Mixer.Posts do
|
|||||||
rpc_action :update_tweet, :update
|
rpc_action :update_tweet, :update
|
||||||
rpc_action :destroy_tweet, :destroy
|
rpc_action :destroy_tweet, :destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resource Mixer.Posts.Media do
|
||||||
|
rpc_action :read_media, :read
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
81
lib/mixer/posts/media.ex
Normal file
81
lib/mixer/posts/media.ex
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
defmodule Mixer.Posts.Media do
|
||||||
|
use Ash.Resource,
|
||||||
|
otp_app: :mixer,
|
||||||
|
domain: Mixer.Posts,
|
||||||
|
data_layer: AshPostgres.DataLayer,
|
||||||
|
authorizers: [Ash.Policy.Authorizer],
|
||||||
|
extensions: [
|
||||||
|
AshTypescript.Resource
|
||||||
|
]
|
||||||
|
|
||||||
|
postgres do
|
||||||
|
table "media"
|
||||||
|
repo Mixer.Repo
|
||||||
|
end
|
||||||
|
|
||||||
|
typescript do
|
||||||
|
type_name "media"
|
||||||
|
end
|
||||||
|
|
||||||
|
actions do
|
||||||
|
defaults [:read]
|
||||||
|
|
||||||
|
create :upload do
|
||||||
|
accept [:s3_key]
|
||||||
|
change relate_actor(:user)
|
||||||
|
end
|
||||||
|
|
||||||
|
update :link_to_tweet do
|
||||||
|
accept [:tweet_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
destroy :destroy do
|
||||||
|
primary? true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
|
||||||
|
attribute :s3_key, :string do
|
||||||
|
allow_nil? false
|
||||||
|
public? true
|
||||||
|
end
|
||||||
|
|
||||||
|
attribute :user_id, :uuid do
|
||||||
|
allow_nil? false
|
||||||
|
public? true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
relationships do
|
||||||
|
belongs_to :user, Mixer.Accounts.User do
|
||||||
|
attribute_writable? true
|
||||||
|
allow_nil? false
|
||||||
|
public? true
|
||||||
|
end
|
||||||
|
|
||||||
|
belongs_to :tweet, Mixer.Posts.Tweet do
|
||||||
|
allow_nil? true
|
||||||
|
public? true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
policies do
|
||||||
|
policy action_type(:read) do
|
||||||
|
authorize_if always()
|
||||||
|
end
|
||||||
|
|
||||||
|
policy action(:upload) do
|
||||||
|
authorize_if actor_present()
|
||||||
|
end
|
||||||
|
|
||||||
|
policy action(:link_to_tweet) do
|
||||||
|
authorize_if relates_to_actor_via(:user)
|
||||||
|
end
|
||||||
|
|
||||||
|
policy action_type(:destroy) do
|
||||||
|
authorize_if relates_to_actor_via(:user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
24
lib/mixer/posts/media_uploader.ex
Normal file
24
lib/mixer/posts/media_uploader.ex
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
defmodule Mixer.Posts.MediaUploader do
|
||||||
|
use Waffle.Definition
|
||||||
|
|
||||||
|
@async false
|
||||||
|
@versions [:original]
|
||||||
|
@extensions ~w(.jpg .jpeg .png .gif .webp .mp4 .mov)
|
||||||
|
|
||||||
|
def validate({file, _scope}) do
|
||||||
|
ext = file.file_name |> Path.extname() |> String.downcase()
|
||||||
|
if ext in @extensions, do: :ok, else: {:error, "unsupported file type #{ext}"}
|
||||||
|
end
|
||||||
|
|
||||||
|
def storage_dir(_version, {_file, scope}), do: "uploads/media/#{scope.id}"
|
||||||
|
|
||||||
|
def filename(_version, {file, _scope}) do
|
||||||
|
Path.basename(file.file_name, Path.extname(file.file_name))
|
||||||
|
end
|
||||||
|
|
||||||
|
def s3_object_headers(_version, {file, _scope}) do
|
||||||
|
[content_type: MIME.from_path(file.file_name)]
|
||||||
|
end
|
||||||
|
|
||||||
|
def acl(_version, _), do: :public_read
|
||||||
|
end
|
||||||
@@ -30,8 +30,25 @@ defmodule Mixer.Posts.Tweet do
|
|||||||
create :create do
|
create :create do
|
||||||
upsert? true
|
upsert? true
|
||||||
accept [:content]
|
accept [:content]
|
||||||
|
argument :media_id, :uuid, allow_nil?: true
|
||||||
change relate_actor(:user)
|
change relate_actor(:user)
|
||||||
change transition_state(:posted)
|
change transition_state(:posted)
|
||||||
|
change fn changeset, context ->
|
||||||
|
case Ash.Changeset.get_argument(changeset, :media_id) do
|
||||||
|
nil ->
|
||||||
|
changeset
|
||||||
|
|
||||||
|
media_id ->
|
||||||
|
Ash.Changeset.after_action(changeset, fn _changeset, tweet ->
|
||||||
|
Mixer.Posts.Media
|
||||||
|
|> Ash.get!(media_id, authorize?: false)
|
||||||
|
|> Ash.Changeset.for_update(:link_to_tweet, %{tweet_id: tweet.id}, actor: context.actor)
|
||||||
|
|> Ash.update!()
|
||||||
|
|
||||||
|
{:ok, tweet}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -43,6 +60,12 @@ defmodule Mixer.Posts.Tweet do
|
|||||||
public? true
|
public? true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attribute :likes, :integer do
|
||||||
|
allow_nil? false
|
||||||
|
default 0
|
||||||
|
public? true
|
||||||
|
end
|
||||||
|
|
||||||
attribute :user_id, :uuid do
|
attribute :user_id, :uuid do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
public? true
|
||||||
@@ -56,6 +79,10 @@ defmodule Mixer.Posts.Tweet do
|
|||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
public? true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
has_many :media, Mixer.Posts.Media do
|
||||||
|
public? true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
policies do
|
policies do
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ defmodule MixerWeb.PageController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
|
asset_host = Application.get_env(:waffle, :asset_host, "http://localhost:3900")
|
||||||
|
bucket = Application.get_env(:waffle, :bucket, "mixer-bucket")
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_root_layout(html: {MixerWeb.Layouts, :spa_root})
|
|> put_root_layout(html: {MixerWeb.Layouts, :spa_root})
|
||||||
|> render(:index, current_user: conn.assigns[:current_user])
|
|> render(:index,
|
||||||
|
current_user: conn.assigns[:current_user],
|
||||||
|
media_host: "#{asset_host}/#{bucket}"
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<div id="app"
|
<div id="app"
|
||||||
data-current-user-id={if @current_user, do: @current_user.id, else: ""}
|
data-current-user-id={if @current_user, do: @current_user.id, else: ""}
|
||||||
data-current-user-email={if @current_user, do: @current_user.email, else: ""}>
|
data-current-user-email={if @current_user, do: @current_user.email, else: ""}
|
||||||
|
data-asset-host={@media_host}>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
47
lib/mixer_web/controllers/upload_controller.ex
Normal file
47
lib/mixer_web/controllers/upload_controller.ex
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
defmodule MixerWeb.UploadController do
|
||||||
|
use MixerWeb, :controller
|
||||||
|
|
||||||
|
alias Mixer.Posts.MediaUploader
|
||||||
|
|
||||||
|
def create(conn, %{"file" => %Plug.Upload{} = upload}) do
|
||||||
|
actor = conn.assigns[:current_user]
|
||||||
|
|
||||||
|
unless actor do
|
||||||
|
conn
|
||||||
|
|> put_status(:unauthorized)
|
||||||
|
|> json(%{error: "authentication required"})
|
||||||
|
else
|
||||||
|
scope = %{id: Ash.UUID.generate()}
|
||||||
|
|
||||||
|
case MediaUploader.store({upload, scope}) do
|
||||||
|
{:ok, file_name} ->
|
||||||
|
s3_key = "uploads/media/#{scope.id}/#{file_name}"
|
||||||
|
url = MediaUploader.url({file_name, scope})
|
||||||
|
|
||||||
|
Mixer.Posts.Media
|
||||||
|
|> Ash.Changeset.for_create(:upload, %{s3_key: s3_key}, actor: actor)
|
||||||
|
|> Ash.create()
|
||||||
|
|> case do
|
||||||
|
{:ok, media} ->
|
||||||
|
json(conn, %{success: true, mediaId: media.id, url: url})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> json(%{success: false, error: inspect(error)})
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> json(%{success: false, error: reason})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(conn, _params) do
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "no file provided"})
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -56,7 +56,8 @@ defmodule MixerWeb.Endpoint do
|
|||||||
plug Plug.Parsers,
|
plug Plug.Parsers,
|
||||||
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser, AshJsonApi.Plug.Parser],
|
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser, AshJsonApi.Plug.Parser],
|
||||||
pass: ["*/*"],
|
pass: ["*/*"],
|
||||||
json_decoder: Phoenix.json_library()
|
json_decoder: Phoenix.json_library(),
|
||||||
|
length: 10_000_000
|
||||||
|
|
||||||
plug Plug.MethodOverride
|
plug Plug.MethodOverride
|
||||||
plug Plug.Head
|
plug Plug.Head
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ defmodule MixerWeb.Router do
|
|||||||
get "/feed", PageController, :index
|
get "/feed", PageController, :index
|
||||||
post "/rpc/run", AshTypescriptRpcController, :run
|
post "/rpc/run", AshTypescriptRpcController, :run
|
||||||
post "/rpc/validate", AshTypescriptRpcController, :validate
|
post "/rpc/validate", AshTypescriptRpcController, :validate
|
||||||
|
post "/upload", UploadController, :create
|
||||||
auth_routes AuthController, Mixer.Accounts.User, path: "/auth"
|
auth_routes AuthController, Mixer.Accounts.User, path: "/auth"
|
||||||
sign_out_route AuthController
|
sign_out_route AuthController
|
||||||
|
|
||||||
|
|||||||
59
lib/mixer_web/uploaders/media.ex
Normal file
59
lib/mixer_web/uploaders/media.ex
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
defmodule Mixer.Media do
|
||||||
|
use Waffle.Definition
|
||||||
|
|
||||||
|
# Include ecto support (requires package waffle_ecto installed):
|
||||||
|
# use Waffle.Ecto.Definition
|
||||||
|
|
||||||
|
@versions [:original]
|
||||||
|
|
||||||
|
# To add a thumbnail version:
|
||||||
|
# @versions [:original, :thumb]
|
||||||
|
|
||||||
|
# Override the bucket on a per definition basis:
|
||||||
|
# def bucket do
|
||||||
|
# :custom_bucket_name
|
||||||
|
# end
|
||||||
|
|
||||||
|
# def bucket({_file, scope}) do
|
||||||
|
# scope.bucket || bucket()
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Whitelist file extensions:
|
||||||
|
# def validate({file, _}) do
|
||||||
|
# file_extension = file.file_name |> Path.extname() |> String.downcase()
|
||||||
|
#
|
||||||
|
# case Enum.member?(~w(.jpg .jpeg .gif .png), file_extension) do
|
||||||
|
# true -> :ok
|
||||||
|
# false -> {:error, "invalid file type"}
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Define a thumbnail transformation:
|
||||||
|
# def transform(:thumb, _) do
|
||||||
|
# {:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png}
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Override the persisted filenames:
|
||||||
|
# def filename(version, _) do
|
||||||
|
# version
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Override the storage directory:
|
||||||
|
# def storage_dir(version, {file, scope}) do
|
||||||
|
# "uploads/user/avatars/#{scope.id}"
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Provide a default URL if there hasn't been a file uploaded
|
||||||
|
# def default_url(version, scope) do
|
||||||
|
# "/images/avatars/default_#{version}.png"
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Specify custom headers for s3 objects
|
||||||
|
# Available options are [:cache_control, :content_disposition,
|
||||||
|
# :content_encoding, :content_length, :content_type,
|
||||||
|
# :expect, :expires, :storage_class, :website_redirect_location]
|
||||||
|
#
|
||||||
|
# def s3_object_headers(version, {file, scope}) do
|
||||||
|
# [content_type: MIME.from_path(file.file_name)]
|
||||||
|
# end
|
||||||
|
end
|
||||||
8
mix.exs
8
mix.exs
@@ -85,7 +85,13 @@ defmodule Mixer.MixProject do
|
|||||||
{:gettext, "~> 1.0"},
|
{:gettext, "~> 1.0"},
|
||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
{:dns_cluster, "~> 0.2.0"},
|
{:dns_cluster, "~> 0.2.0"},
|
||||||
{:bandit, "~> 1.5"}
|
{:bandit, "~> 1.5"},
|
||||||
|
{:waffle, "~> 1.1.10"},
|
||||||
|
{:waffle_ecto, "~> 0.0"},
|
||||||
|
{:ex_aws, "~> 2.1.2"},
|
||||||
|
{:ex_aws_s3, "~> 2.0"},
|
||||||
|
{:hackney, "~> 1.9"},
|
||||||
|
{:sweet_xml, "~> 0.6"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
11
mix.lock
11
mix.lock
@@ -19,6 +19,7 @@
|
|||||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
||||||
"castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"},
|
"castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"},
|
||||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||||
|
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
|
||||||
"cinder": {:hex, :cinder, "0.12.1", "02ae4988e025fb32c37e4e7f2e491586b952918c0dd99d856da13271cd680e16", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.3", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "a48b5677c1f57619d9d7564fb2bd7928f93750a2e8c0b1b145852a30ecf2aa20"},
|
"cinder": {:hex, :cinder, "0.12.1", "02ae4988e025fb32c37e4e7f2e491586b952918c0dd99d856da13271cd680e16", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.3", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "a48b5677c1f57619d9d7564fb2bd7928f93750a2e8c0b1b145852a30ecf2aa20"},
|
||||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||||
"conv_case": {:hex, :conv_case, "0.2.3", "c1455c27d3c1ffcdd5f17f1e91f40b8a0bc0a337805a6e8302f441af17118ed8", [:mix], [], "hexpm", "88f29a3d97d1742f9865f7e394ed3da011abb7c5e8cc104e676fdef6270d4b4a"},
|
"conv_case": {:hex, :conv_case, "0.2.3", "c1455c27d3c1ffcdd5f17f1e91f40b8a0bc0a337805a6e8302f441af17118ed8", [:mix], [], "hexpm", "88f29a3d97d1742f9865f7e394ed3da011abb7c5e8cc104e676fdef6270d4b4a"},
|
||||||
@@ -32,12 +33,15 @@
|
|||||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
"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"},
|
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||||
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
||||||
|
"ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"},
|
||||||
|
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.9", "862b7792f2e60d7010e2920d79964e3fab289bc0fd951b0ba8457a3f7f9d1199", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "a480d2bb2da64610014021629800e1e9457ca5e4a62f6775bffd963360c2bf90"},
|
||||||
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
||||||
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
||||||
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [: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", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
|
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [: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", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
|
||||||
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
|
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
|
||||||
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
|
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
|
||||||
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
||||||
|
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
|
||||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
|
"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"},
|
"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"},
|
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||||
@@ -50,13 +54,16 @@
|
|||||||
"langchain": {:hex, :langchain, "0.6.3", "88794ef059c97521279996571ac77daf15f8ccf7c6ccad2716d969cbb052d6ca", [:mix], [{:abacus, "~> 2.1.0", [hex: :abacus, repo: "hexpm", optional: true]}, {:dotenvy, "~> 1.1", [hex: :dotenvy, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26.2 or ~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: true]}, {:nx, ">= 0.7.0", [hex: :nx, repo: "hexpm", optional: true]}, {:req, ">= 0.5.3", [hex: :req, repo: "hexpm", optional: false]}, {:req_llm, "~> 1.6", [hex: :req_llm, repo: "hexpm", optional: true]}], "hexpm", "9f0eeac704bc1b01fb91e0ae38559f9e643b2e067c8e98c43ba06f2213aa14f4"},
|
"langchain": {:hex, :langchain, "0.6.3", "88794ef059c97521279996571ac77daf15f8ccf7c6ccad2716d969cbb052d6ca", [:mix], [{:abacus, "~> 2.1.0", [hex: :abacus, repo: "hexpm", optional: true]}, {:dotenvy, "~> 1.1", [hex: :dotenvy, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26.2 or ~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: true]}, {:nx, ">= 0.7.0", [hex: :nx, repo: "hexpm", optional: true]}, {:req, ">= 0.5.3", [hex: :req, repo: "hexpm", optional: false]}, {:req_llm, "~> 1.6", [hex: :req_llm, repo: "hexpm", optional: true]}], "hexpm", "9f0eeac704bc1b01fb91e0ae38559f9e643b2e067c8e98c43ba06f2213aa14f4"},
|
||||||
"lazy_html": {:hex, :lazy_html, "0.1.10", "ffe42a0b4e70859cf21a33e12a251e0c76c1dff76391609bd56702a0ef5bc429", [: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", "50f67e5faa09d45a99c1ddf3fac004f051997877dc8974c5797bb5ccd8e27058"},
|
"lazy_html": {:hex, :lazy_html, "0.1.10", "ffe42a0b4e70859cf21a33e12a251e0c76c1dff76391609bd56702a0ef5bc429", [: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", "50f67e5faa09d45a99c1ddf3fac004f051997877dc8974c5797bb5ccd8e27058"},
|
||||||
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
||||||
|
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||||
|
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
|
||||||
"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"},
|
"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_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||||
"open_api_spex": {:hex, :open_api_spex, "3.22.2", "0b3c4f572ee69cb6c936abf426b9d84d8eebd34960871fd77aead746f0d69cb0", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "0a4fc08472d75e9cfe96e0748c6b1565b3b4398f97bf43fcce41b41b6fd3fb33"},
|
"open_api_spex": {:hex, :open_api_spex, "3.22.2", "0b3c4f572ee69cb6c936abf426b9d84d8eebd34960871fd77aead746f0d69cb0", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "0a4fc08472d75e9cfe96e0748c6b1565b3b4398f97bf43fcce41b41b6fd3fb33"},
|
||||||
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
|
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
|
||||||
|
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||||
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [: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", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
|
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [: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", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
|
||||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
||||||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||||
@@ -79,7 +86,9 @@
|
|||||||
"spark": {:hex, :spark, "2.6.1", "b0100216d3883c6a281cb2434af45afbd808695aadb034923cbaf7d8a2ba46ab", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "77bbefa5263bb6b70e1195bc0fc662ddb8ef5937a356a77ae072e56983ad13f0"},
|
"spark": {:hex, :spark, "2.6.1", "b0100216d3883c6a281cb2434af45afbd808695aadb034923cbaf7d8a2ba46ab", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "77bbefa5263bb6b70e1195bc0fc662ddb8ef5937a356a77ae072e56983ad13f0"},
|
||||||
"spitfire": {:hex, :spitfire, "0.3.10", "19aea9914132456515e8f7d592f63ab9f3130876b0252e834d2390bdd8becb24", [:mix], [], "hexpm", "6a6a5f77eb4165249c76199cd2d01fb595bac9207aed3de551918ac1c2bc9267"},
|
"spitfire": {:hex, :spitfire, "0.3.10", "19aea9914132456515e8f7d592f63ab9f3130876b0252e834d2390bdd8becb24", [:mix], [], "hexpm", "6a6a5f77eb4165249c76199cd2d01fb595bac9207aed3de551918ac1c2bc9267"},
|
||||||
"splode": {:hex, :splode, "0.3.0", "ff8effecc509a51245df2f864ec78d849248647c37a75886033e3b1a53ca9470", [:mix], [], "hexpm", "73cfd0892d7316d6f2c93e6e8784bd6e137b2aa38443de52fd0a25171d106d81"},
|
"splode": {:hex, :splode, "0.3.0", "ff8effecc509a51245df2f864ec78d849248647c37a75886033e3b1a53ca9470", [:mix], [], "hexpm", "73cfd0892d7316d6f2c93e6e8784bd6e137b2aa38443de52fd0a25171d106d81"},
|
||||||
|
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||||
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
|
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
|
||||||
|
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||||
"swoosh": {:hex, :swoosh, "1.24.0", "4df9645aeeef925a2eb10f7a588a6a09ddd6d370c5dfbd3e821b699c574bdf57", [: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", "6ddd84550800468d0e2c15a8aaff924a64c014ed6cff90318077efd1672b8b3b"},
|
"swoosh": {:hex, :swoosh, "1.24.0", "4df9645aeeef925a2eb10f7a588a6a09ddd6d370c5dfbd3e821b699c574bdf57", [: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", "6ddd84550800468d0e2c15a8aaff924a64c014ed6cff90318077efd1672b8b3b"},
|
||||||
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
||||||
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
|
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
|
||||||
@@ -89,6 +98,8 @@
|
|||||||
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
"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"},
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
|
||||||
"usage_rules": {:hex, :usage_rules, "1.2.5", "3737b44ecba9fa816e04abadf5199bc78b68f05d49ae79f1fa785342167a721f", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "509eb67b7a7f90514889b602c41692b45956dc3e381ca0e25505dccd8de90390"},
|
"usage_rules": {:hex, :usage_rules, "1.2.5", "3737b44ecba9fa816e04abadf5199bc78b68f05d49ae79f1fa785342167a721f", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "509eb67b7a7f90514889b602c41692b45956dc3e381ca0e25505dccd8de90390"},
|
||||||
|
"waffle": {:hex, :waffle, "1.1.10", "0f847ed6f95349af258a90f0f70ffea02b3d3729c4eb78f6fae7bf776e91779e", [:mix], [{:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.1", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "859ba6377b78f0a51bc9596227b194f26241efbbd408bd217450c22b0f359cc4"},
|
||||||
|
"waffle_ecto": {:hex, :waffle_ecto, "0.0.12", "e5c17c49b071b903df71861c642093281123142dc4e9908c930db3e06795b040", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:waffle, "~> 1.0", [hex: :waffle, repo: "hexpm", optional: false]}], "hexpm", "585fe6371057066d2e8e3383ddd7a2437ff0668caf3f4cbf5a041e0de9837168"},
|
||||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
"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"},
|
"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"},
|
||||||
"xema": {:hex, :xema, "0.17.7", "7eeda174b70a5f7fb1cc2e9fa3a7d4e78e206a99866c107d477309410b678cf2", [:mix], [{:conv_case, "~> 0.2.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "7e3d7c0629282c21af6aaa5e2ba593218cd764a57bd1ae49e2c4412324e904cd"},
|
"xema": {:hex, :xema, "0.17.7", "7eeda174b70a5f7fb1cc2e9fa3a7d4e78e206a99866c107d477309410b678cf2", [:mix], [{:conv_case, "~> 0.2.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "7e3d7c0629282c21af6aaa5e2ba593218cd764a57bd1ae49e2c4412324e904cd"},
|
||||||
|
|||||||
38
priv/repo/migrations/20260330173403_add_posts_media_s3.exs
Normal file
38
priv/repo/migrations/20260330173403_add_posts_media_s3.exs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
defmodule Mixer.Repo.Migrations.AddPostsMediaS3 do
|
||||||
|
@moduledoc """
|
||||||
|
Updates resources based on their most recent snapshots.
|
||||||
|
|
||||||
|
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table(:tweets) do
|
||||||
|
add :likes, :bigint, null: false, default: 0
|
||||||
|
end
|
||||||
|
|
||||||
|
create table(:media, primary_key: false) do
|
||||||
|
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
|
||||||
|
add :s3_key, :text, null: false
|
||||||
|
|
||||||
|
add :tweet_id,
|
||||||
|
references(:tweets,
|
||||||
|
column: :id,
|
||||||
|
name: "media_tweet_id_fkey",
|
||||||
|
type: :uuid,
|
||||||
|
prefix: "public"
|
||||||
|
), null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
drop constraint(:media, "media_tweet_id_fkey")
|
||||||
|
|
||||||
|
drop table(:media)
|
||||||
|
|
||||||
|
alter table(:tweets) do
|
||||||
|
remove :likes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
defmodule Mixer.Repo.Migrations.AddUserIdToMediaAndAllowNullTweetId do
|
||||||
|
@moduledoc """
|
||||||
|
Updates resources based on their most recent snapshots.
|
||||||
|
|
||||||
|
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table(:media) do
|
||||||
|
modify :tweet_id, :uuid, null: true
|
||||||
|
|
||||||
|
add :user_id,
|
||||||
|
references(:users,
|
||||||
|
column: :id,
|
||||||
|
name: "media_user_id_fkey",
|
||||||
|
type: :uuid,
|
||||||
|
prefix: "public"
|
||||||
|
), null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
drop constraint(:media, "media_user_id_fkey")
|
||||||
|
|
||||||
|
alter table(:media) do
|
||||||
|
remove :user_id
|
||||||
|
modify :tweet_id, :uuid, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
75
priv/resource_snapshots/repo/media/20260330173404.json
Normal file
75
priv/resource_snapshots/repo/media/20260330173404.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"gen_random_uuid()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "id",
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "s3_key",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"deferrable": false,
|
||||||
|
"destination_attribute": "id",
|
||||||
|
"destination_attribute_default": null,
|
||||||
|
"destination_attribute_generated": null,
|
||||||
|
"index?": false,
|
||||||
|
"match_type": null,
|
||||||
|
"match_with": null,
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "media_tweet_id_fkey",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"schema": "public",
|
||||||
|
"table": "tweets"
|
||||||
|
},
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "tweet_id",
|
||||||
|
"type": "uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"create_table_options": null,
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "D707EF6F25A8BD5E01A333A9C8CBE9AAAF015CFB31BF4F5A4DE4CDA2CD9A4DD8",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.Mixer.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "media"
|
||||||
|
}
|
||||||
106
priv/resource_snapshots/repo/media/20260330174802.json
Normal file
106
priv/resource_snapshots/repo/media/20260330174802.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"gen_random_uuid()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "id",
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "s3_key",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"deferrable": false,
|
||||||
|
"destination_attribute": "id",
|
||||||
|
"destination_attribute_default": null,
|
||||||
|
"destination_attribute_generated": null,
|
||||||
|
"index?": false,
|
||||||
|
"match_type": null,
|
||||||
|
"match_with": null,
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "media_user_id_fkey",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"schema": "public",
|
||||||
|
"table": "users"
|
||||||
|
},
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "user_id",
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"deferrable": false,
|
||||||
|
"destination_attribute": "id",
|
||||||
|
"destination_attribute_default": null,
|
||||||
|
"destination_attribute_generated": null,
|
||||||
|
"index?": false,
|
||||||
|
"match_type": null,
|
||||||
|
"match_with": null,
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "media_tweet_id_fkey",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"schema": "public",
|
||||||
|
"table": "tweets"
|
||||||
|
},
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "tweet_id",
|
||||||
|
"type": "uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"create_table_options": null,
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "8E26F7D27DBFD094DC6AA9EDA77C4D40767CF9F3B738CF6F31839A68AD62A886",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.Mixer.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "media"
|
||||||
|
}
|
||||||
99
priv/resource_snapshots/repo/tweets/20260330173405.json
Normal file
99
priv/resource_snapshots/repo/tweets/20260330173405.json
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"gen_random_uuid()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "id",
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "content",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "0",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "likes",
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"deferrable": false,
|
||||||
|
"destination_attribute": "id",
|
||||||
|
"destination_attribute_default": null,
|
||||||
|
"destination_attribute_generated": null,
|
||||||
|
"index?": false,
|
||||||
|
"match_type": null,
|
||||||
|
"match_with": null,
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "tweets_user_id_fkey",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"schema": "public",
|
||||||
|
"table": "users"
|
||||||
|
},
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "user_id",
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "\"drafted\"",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "state",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"create_table_options": null,
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "72C986AFFFB09E7394DC54328AC3F7E47C24599B894E1E65964132E2598AB55C",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.Mixer.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "tweets"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user