I'm a creating a blog using Remix.
Remix supports MDX as a route, which is perfect for me, as I can just write my blog posts as .mdx files and they'll naturally become routes.
However, if you access the index route - I would like to display the list of links to all the articles, (ideally sorted by creation date, or some kind of metadata in the .mdx file).
I can't see a natural way to do this in the Remix documentation.
Best solution I've got is that I would run a script as part of my build process, that the examines the routes/ folder and generates a TableOfContents.tsx file, which the index route can use.
Is there an out of the box solution?
The remix documentation does hint at this, but doesn't appear to provide a solid suggestion.
Clearly this is not a scalable solution for a blog with thousands of posts. Realistically speaking, writing is hard, so if your blog starts to suffer from too much content, that's an awesome problem to have. If you get to 100 posts (congratulations!), we suggest you rethink your strategy and turn your posts into data stored in a database so that you don't have to rebuild and redeploy your blog every time you fix a typo.
(Personally, I don't think redeploying each time you fix a typo is too much of a problem, it's more I don't want to have to manually add links everytime I add a new post).
For posterity, I'll post my current solution.
I run this script at build time, and it inspects all the blog posts and creates a <ListOfArticles> components.
import { readdir, writeFile, appendFile } from 'node:fs/promises';
const PATH_TO_ARTICLES_COMPONENT = "app/generated/ListOfArticles.tsx";
const PATH_TO_BLOG_POSTS = "app/routes/posts"
async function generateListOfArticles() {
try {
const files = await readdir(PATH_TO_BLOG_POSTS);
await writeFile(PATH_TO_ARTICLES_COMPONENT, `
export const ListOfArticles = () => {
return <ul>`);
for (const file of files) {
if (!file.endsWith(".mdx")) {
console.warn(`Found a non-mdx file in ${PATH_TO_BLOG_POSTS}: ${file}`)
}
else {
const fileName = file.split('.mdx')[0];
await appendFile(PATH_TO_ARTICLES_COMPONENT, `
<li>
<a
href="/posts/${fileName}"
>
${fileName} </a>
</li>
`)
}
}
appendFile(PATH_TO_ARTICLES_COMPONENT, `
</ul>
}
`)
} catch (err) {
throw err;
}
}
generateListOfArticles().then(() => {
console.info(`Successfully generated ${PATH_TO_ARTICLES_COMPONENT}`)
})
Is quite rudimentary - could be extended by inspecting the metadata section of the MDX file to get a proper title, doing sort order, etc.
This is not exactly what you were looking for but I used your solution as inspiration to figure out a solution to my own problem: Generating a type for routes so that the type checker can make sure there aren't broken links hanging around in the app.
Here's my script, generate-app-path-type.ts:
import { appendFile, readdir, writeFile } from "node:fs/promises";
const PATH_TO_APP_PATHS = "app/utils/app-path.generated.ts";
const PATHS_PATH = "app/routes";
async function collectFiles(currentPaths: string[], currentPath = "") {
const glob = await readdir(PATHS_PATH + currentPath, { withFileTypes: true });
for (const entry of glob) {
if (entry.isFile()) {
currentPaths.push(currentPath + "/" + entry.name);
} else if (entry.isDirectory()) {
await collectFiles(currentPaths, currentPath + "/" + entry.name);
}
}
}
// NOTE: the `withoutNamespacing` regex only removes top level namespacing like routes/__auth but
// wouldn't catch namespacing like routes/my/__new-namespace
function filenameToRoute(filename: string): string {
const withoutExtension = filename.split(/.tsx?$/)[0];
const withoutIndex = withoutExtension.split(/\/index$/)[0];
const withoutNamespacing = withoutIndex.replace(/^\/__\w+/, "");
// Return root path in the case of "index.tsx"
return withoutNamespacing || "/";
}
async function generateAppPathType() {
const filenames: string[] = [];
await collectFiles(filenames);
await writeFile(PATH_TO_APP_PATHS, `export type AppPath =\n`);
const remixRoutes = filenames.slice(0, filenames.length - 1).map(filenameToRoute);
// only unique routes, and alphabetized please
const routes = [...new Set(remixRoutes)].sort();
for (const route of routes) {
await appendFile(PATH_TO_APP_PATHS, ` | "${route}"\n`);
}
await appendFile(
PATH_TO_APP_PATHS,
` | "${filenameToRoute(filenames[filenames.length - 1])}";\n`,
);
}
generateAppPathType().then(() => {
// eslint-disable-next-line no-console
console.info(`Successfully generated ${PATH_TO_APP_PATHS}`);
});
The script generates a type like:
export type AppPath =
| "/"
| "/login";
I use this type with a utility file:
import type { AppPath } from "./app-path.generated";
import { SERVER_URL } from "./env";
export function p(path: AppPath): AppPath {
return path;
}
export function url(path: AppPath): string {
return new URL(path, SERVER_URL).toString();
}
Finally, whenever I need a path (like when I redirect a user), I can do this which is a little more typing than the string literal but is a confirmed URL according to the type checker:
p("/login") // or url("/login")
Related
I have a function, addWord(word:string), which prohibits the same Context.sender from adding more than one consecutive word. I store the last signee in the contract. Forgive me if there are some syntax errors in the code below, I only added parts of my current code.
export class Contract {
private lastSignedBy: string = '';
private words: PersistentVector<string> = new PersistentVector<string>('w');
#mutateState()
addWord(word: string): number {
// can't add more than one consecutive word
if(this.lastSignedBy != Context.sender){
this.words.push(word);
this.lastSignedBy = Context.sender;
return 1;
}
return 0;
}
}
Now, I want to test this function, but I don't know how to change the sender in the test. I think Maybe I need to mock the Context, but I'm not sure how to do that. The test framework is as-pect
Here is my current test.
let contract: Contract;
beforeEach(() => {
contract = new Contract();
});
describe('Contract', () => {
it('can add word if different sender than last time', () => {
expect(contract.addWord('word one')).toStrictEqual(1);
// TODO change Context.sender somehow.
expect(contract.addWord('second word')).toStrictEqual(1);
});
}
You can use a reference to VMContext like this
import { VMContext } from "near-sdk-as";
...
it("provides access to most recent player", () => {
VMContext.setSigner_account_id('player1')
});
https://github.com/Learn-NEAR/NCD.L1.sample--lottery/blob/2bd11bc1092004409e32b75736f78adee821f35b/src/lottery/__tests__/index.unit.spec.ts#L68
Using the Debut theme in Shopify, I have a product with variants that are not compatible. At present they show as unavailable but I want to hide them totally so that only valid options show with each other. So as example if I have Red, Blue, Green shoes and have sizes 9,10,11. But can only get Red shoes in Size 10. I don't want to see options for 9 or 11 ever.
Online someone pointed to theme.js and the code below, but I'm not sure what to change.
Thanks
$(this.singleOptionSelector, this.$container).on(
'change',
this._onSelectChange.bind(this)
);
}
I've just spend most of the day on this, and have come up with the following, which seems to work nicely on the site I'm developing.
The answer I came up with involves editing the assets/theme.js file. At present, the code below disables the select options which are not relevant by checking them against the available variant combinations, but you could easily adapt the below to hide them and then show them instead with CSS.
assets/theme.js
The _hideUnavailableOptions method below needs to be added to the Variants.prototype object.
You then need to call the method from two different places, see below.
_hideUnavailableOptions: function() {
const option1 = document.getElementById("SingleOptionSelector-0");
const option2 = document.getElementById("SingleOptionSelector-1");
if (option2) {
const secondOptions = option2.options;
const variants = this.product.variants;
let possibles = [];
variants.forEach((variant) => {
if (variant.options.includes(option1.value)) {
possibles.push(variant.options)
}
})
for (let option of secondOptions) {
const value = option.value;
let flag = false;
possibles.forEach((possible) => {
if (possible.includes(value)) {
flag = true;
}
})
if (flag === false) {
option.removeAttribute('selected');
option.setAttribute('disabled', 'disabled');
} else {
option.removeAttribute('disabled');
}
} option2.querySelector(':not([disabled="disabled"])').setAttribute('selected', 'selected');
}
},
Call the method as follows:
function Variants(options) {
//stuff before this, then...
this.enableHistoryState = options.enableHistoryState;
this._hideUnavailableOptions(); //N.B. this MUST be before the next line
this.currentVariant = this._getVariantFromOptions();
}
...and again, call the method from Variants.prototype._onSelectChange() - make sure it's the first line in there...
_onSelectChange: function() {
let hideUnavailable = this._hideUnavailableOptions(); //N.B. this MUST be before the next line
var variant = this._getVariantFromOptions();
//lots more stuff follows...
}
Is there a way in JXA to get multiple properties from multiple objects with a single call?
For example, I want to get name and enabled property from menu items which can be done for each individual property as follows:
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.name()
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.enabled()
but is it possible to get them with a single function call? Something like:
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.select('name', 'enabled')
I know, that I can iterate through the menuBarItems and collect properties from .properties() method, but this approach is too slow, that's why I'm looking for other options.
UPDATE
I'm looking for better performance, not for nicer syntax, i.e. I want properties to be retrieved in a single call to System Events.
I'd probably do it like this:
sys = Application('com.apple.systemevents');
FinderProc = sys.processes['Finder'];
FinderMenuBarItems = FinderProc.menuBars[0].menuBarItems();
Array.from(FinderMenuBarItems,x=>[x.name(),x.enabled()]);
By first converting the object to an array, this allows one to map each element and retrieve the desired properties for all in one go. The code is split over several lines for ease of reading.
EDIT: added on 2019-07-27
Following on from your comment regarding Objective-C implementation, I had a bit of time today to write a JSObjc script. It does the same thing as the vanilla JXA version above, and, yes, it clearly makes multiple function calls, which is necessary. But it's performing these functions at a lower level than System Events (which isn't involved at all here), so hopefully you'll find it more performant.
ObjC.import('ApplicationServices');
ObjC.import('CoreFoundation');
ObjC.import('Foundation');
ObjC.import('AppKit');
var err = {
'-25211':'APIDisabled',
'-25206':'ActionUnsupported',
'-25205':'AttributeUnsupported',
'-25204':'CannotComplete',
'-25200':'Failure',
'-25201':'IllegalArgument',
'-25202':'InvalidUIElement',
'-25203':'InvalidUIElementObserver',
'-25212':'NoValue',
'-25214':'NotEnoughPrecision',
'-25208':'NotImplemented',
'-25209':'NotificationAlreadyRegistered',
'-25210':'NotificationNotRegistered',
'-25207':'NotificationUnsupported',
'-25213':'ParameterizedAttributeUnsupported',
'0':'Success'
};
var unwrap = ObjC.deepUnwrap.bind(ObjC);
var bind = ObjC.bindFunction.bind(ObjC);
bind('CFMakeCollectable', [ 'id', [ 'void *' ] ]);
Ref.prototype.nsObject = function() {
return unwrap($.CFMakeCollectable(this[0]));
}
function getAttrValue(AXUIElement, AXAttrName) {
var e;
var _AXAttrValue = Ref();
e = $.AXUIElementCopyAttributeValue(AXUIElement,
AXAttrName,
_AXAttrValue);
if (err[e]!='Success') return err[e];
return _AXAttrValue.nsObject();
}
function getAttrValues(AXUIElement, AXAttrNames){
var e;
var _AXAttrValues = Ref();
e = $.AXUIElementCopyMultipleAttributeValues(AXUIElement,
AXAttrNames,
0,
_AXAttrValues);
if (err[e]!='Success') return err[e];
return _AXAttrValues.nsObject();
}
function getAttrNames(AXUIElement) {
var e;
var _AXAttrNames = Ref();
e = $.AXUIElementCopyAttributeNames(AXUIElement, _AXAttrNames);
if (err[e]!='Success') return err[e];
return _AXAttrNames.nsObject();
}
(() => {
const pid_1 = $.NSWorkspace.sharedWorkspace
.frontmostApplication
.processIdentifier;
const appElement = $.AXUIElementCreateApplication(pid_1);
const menuBar = getAttrValue(appElement,"AXMenuBar");
const menuBarItems = getAttrValue(menuBar, "AXChildren");
return menuBarItems.map(x => {
return getAttrValues(x, ["AXTitle", "AXEnabled"]);
});
})();
How are we meant to handle path parameters in Angular 2 when doing RESTful calls to a Web Service?
I've found the URLSearchParams object for query parameters but from what I have found it seems we'll have to make do with string-concatenation for the path itself. Like
let url = 'api/v1/something/' + encodeURIComponent(someParameter) +
'/etc/' + encodeURIComponent(anotherParam) + '/and/so/on';
Is there, included in angular2, something that does similar to:
let url = new URL('api/v1/something/{id}/etc/{name}/and/so/on', param1, param2);
I can of course create something similar myself but prefer if there is something included in angular2.
Indeed, you can use string interpolation and nearly exactly the same thing you suggest, assuming id and name are already set and encoded.
let url = new URL(`api/v1/something/${id}/etc/${name}/and/so/on`);
I'll note that currently the ES6 URL class only supports searchParams, but has no notion of path parameters.
Also, I know of no method that will automatically encode your parameters without implementing your own QueryEncoder with URLSearchParams
If anybody is interested, this is what I ended up doing:
export interface PathParameters {
[parameterName: string]: string;
}
export class Url {
public static create(url: string, parameters: PathParameters) {
const placeholders = url.match(/({[a-zA-Z]*})/g);
placeholders.forEach((placeholder: string) => {
const key = placeholder.substr(1, placeholder.length - 2);
const value = parameters[key];
if (!value) {
throw new Error(`parameter ${key} was not provided`);
}
url = url.replace(placeholder, encodeURIComponent(value));
});
return url;
}
}
How to use:
const url = Url.create('/api/cars/{carId}/parts/{partId}', {
carId: carId,
partId: partId
});
This might be a bit too specific for here and I may need to contact redactor support but i've seen other questions about redactor here so i figured i'd give it a shot ...
Ok ...
So i'm trying to get get image uploading to work following the example here ...
http://imperavi.com/redactor/docs/images/
My client side code ...
$("textarea").redactor({
focus: true,
imageUpload: '/MyController/UploadImage'
});
My MVC controller action looks like this ...
public JsonResult UploadImage(object image)
{
// Do something with whatever that was i got from redactor
var result = new { filelink = "" };
return Json(result);
}
The problem is ... what did redactor actually give me?
Was it the whole file? a chunk? i can't seem to tell because the object has no type information at all and the raw post information seems way too little to actually be a whole image file.
Has anyone had any experience with this / actually done it before?
I don't really want to setup php on my server for this 1 function.
EDIT:
Ok a bit more digging reveals that if i pull the underlying Request object it has a files property which apparently contains my posted image file.
I think i might be able to figure it out from here.
Where I get a code block in place i'll post it as an answer.
You are receiving a HttpPostedFileBase object. Here is my implementation:
jQuery:
$('#blog-post').redactor(
{
imageUpload: '/blog/images/',
imageGetJson: '/images/locations/blogs/'
});
Then in the controller:
public ActionResult Images(HttpPostedFileBase file)
{
// Verify that the user selected a file
if( file != null && file.ContentLength > 0 )
{
// extract only the fielname
var fileName = Path.GetFileName( file.FileName );
// store the file
var path = Path.Combine( ImageLocation.BlogPicturePath, fileName );
file.SaveAs( path );
}
return Json( new { filelink = ImageLocation.BlogPictureUrl + "/" + file.FileName } );
}
ok um ... i think im there ...
This needs a bit of cleaning up and I don't expect you guys to understand what goes on under the bonnet of my custom DMS code but just assume it takes the stream and returns a FileInfo object and in theory this should work for you too ...
public ActionResult Upload()
{
// this object is specific to my system but all it does is
// stream the file to a path on the server (code not needed for this Q)
var dmsService = _kernel.Get<IDMSFileSystemService>();
List<FileInfo> savedFiles = new List<FileInfo>();
for (int i = 0; i < Request.Files.Count; i++)
{
var file = Request.Files[i];
using (file.InputStream)
{
savedFiles.Add(dmsService.AddFromStream(file.InputStream, file.FileName);
}
}
var result = savedFiles.Select(f => new { filelink = f.Path}).ToArray();
return Json(result);
}
Suprisingly simple right ... :)